Introduction
The Epsilon3 API is centered around REST principles, using standard HTTP response codes, verbs, and authentication. All API endpoints use resource-oriented URLs and JSON-encoded request and response bodies.
Rate Limiting
The API enforces rate limits on authenticated endpoints. Each client is allowed 15,000 requests per 10-minute window. Rate limits are applied per API key (by team or user) for API-authenticated requests and per user for web-authenticated requests.
When a rate limit is exceeded, the API responds with HTTP status code 429 Too Many Requests. Standard rate limit headers are included in every response:
| Header | Description |
|---|---|
RateLimit-Limit |
Maximum number of requests allowed in the current window |
RateLimit-Remaining |
Number of requests remaining in the current window |
RateLimit-Reset |
Time (in seconds) until the current rate limit window resets |
On-Prem Deployments
The URL to use for requests will vary depending on your setup, it will be the same as your web URL with the addition of the /api postfix, e.g. https://YOUR_WEB_URL/api.
Authentication
The API key is expected as the username component of a basic auth header
export EPSILON3_API_KEY=YOUR_API_KEY
curl -u ${EPSILON3_API_KEY}: ...
$env:EPSILON3_API_KEY = "YOUR_API_KEY"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Headers @{"Authorization" = "Basic $authHeader" ...
# Export API key in shell outside of code with export EPSILON3_API_KEY=YOUR_API_KEY
import os
requests.post(
url,
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
...
)
Authentication is performed using a per-workspace API key. See our guide on how to generate one.
Accounting API
Supports managing the general ledger chart of accounts. These endpoints require the accounting module to be enabled for your workspace; requests return HTTP 404 when it is not. Other accounting entities (journal entries, invoices, payment terms) are not currently exposed via the API.
An account object contains the following fields:
| Field | Value |
|---|---|
| id | String - unique account id |
| account_number | String - account number, unique within the workspace |
| description | String - account description |
| account_type | String - asset | liability | equity | revenue | expense |
| parent_account_id | String or null - id of the parent account when the account is nested |
| starting_balance_minor_units | Integer - starting balance in minor currency units (e.g. cents) |
| is_active | Boolean - false when the account has been deleted (deactivated) |
| created_at | String - ISO8601 datetime the account was created |
| updated_at | String - ISO8601 datetime the account was last updated |
| is_leaf | Boolean - true when the account has no active child accounts |
List Accounts
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/accounting/accounts',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/accounting/accounts \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/accounting/accounts"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/accounting/accounts
Retrieve all accounts in your workspace, sorted by account_number. Only active accounts are returned unless include-inactive is set. All query parameters below are optional and are combined with AND when several are supplied.
| Query parameter | Value |
|---|---|
| include-inactive | Boolean - also return deactivated (deleted) accounts, identifiable by is_active: false |
| include-line-items | Boolean - also return each account's journal line items and ledger totals (see below) |
| account-type | String - asset | liability | equity | revenue | expense |
| search | String - case-insensitive partial match on account number or description |
| created-at-after | String - ISO8601 datetime, accounts created at or after this time |
| created-at-before | String - ISO8601 datetime, accounts created at or before this time |
| updated-at-after | String - ISO8601 datetime, accounts updated at or after this time |
| updated-at-before | String - ISO8601 datetime, accounts updated at or before this time |
When include-line-items=true, each account additionally carries total_debits_minor_units, total_credits_minor_units, ending_balance_minor_units, depth, has_children, and a line_items array of the account's journal entry lines (id, journal_entry_id, account_id, debit_minor_units, credit_minor_units, notes, running_balance_minor_units, created_at, updated_at). Totals on a parent account roll up its child accounts, and are computed across all accounts matching the filters. line_items is only populated on leaf accounts.
Unknown parameters, or values that fail validation (for example an invalid account-type or a malformed date), return HTTP 422.
Example Response Body
{
"accounts": [
{
"id": "6a1b6a70-6b6e-4a5e-9d2e-6a4f1c7f2a01",
"account_number": "1000",
"description": "Assets",
"account_type": "asset",
"parent_account_id": null,
"starting_balance_minor_units": 0,
"is_active": true,
"created_at": "2026-01-15T10:00:00.000Z",
"updated_at": "2026-06-01T12:00:00.000Z",
"is_leaf": false
},
{
"id": "7c2d8b81-7c7f-4b6f-8e3f-7b5a2d8a3b12",
"account_number": "1010",
"description": "Cash",
"account_type": "asset",
"parent_account_id": "6a1b6a70-6b6e-4a5e-9d2e-6a4f1c7f2a01",
"starting_balance_minor_units": 250000,
"is_active": true,
"created_at": "2026-01-15T10:05:00.000Z",
"updated_at": "2026-01-15T10:05:00.000Z",
"is_leaf": true
}
]
}
Get Account
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/accounting/accounts/7c2d8b81-7c7f-4b6f-8e3f-7b5a2d8a3b12',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/accounting/accounts/7c2d8b81-7c7f-4b6f-8e3f-7b5a2d8a3b12 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/accounting/accounts/7c2d8b81-7c7f-4b6f-8e3f-7b5a2d8a3b12"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/accounting/accounts/:accountId
Retrieve a single account by id. Returns HTTP 404 if the account does not exist. A deactivated (deleted) account is still returned, with is_active: false.
Example Response Body
{
"id": "7c2d8b81-7c7f-4b6f-8e3f-7b5a2d8a3b12",
"account_number": "1010",
"description": "Cash",
"account_type": "asset",
"parent_account_id": "6a1b6a70-6b6e-4a5e-9d2e-6a4f1c7f2a01",
"starting_balance_minor_units": 250000,
"is_active": true,
"created_at": "2026-01-15T10:05:00.000Z",
"updated_at": "2026-01-15T10:05:00.000Z",
"is_leaf": true
}
Create Account
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/accounting/accounts',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
'account_number': '1020',
'description': 'Accounts Receivable',
'account_type': 'asset',
'parent_account_id': '6a1b6a70-6b6e-4a5e-9d2e-6a4f1c7f2a01',
'starting_balance_minor_units': 0,
}
)
curl -X POST \
https://api.epsilon3.io/v1/accounting/accounts \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"account_number": "1020",
"description": "Accounts Receivable",
"account_type": "asset",
"parent_account_id": "6a1b6a70-6b6e-4a5e-9d2e-6a4f1c7f2a01",
"starting_balance_minor_units": 0
}'
$apiUrl = "https://api.epsilon3.io/v1/accounting/accounts"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
}
$body = @{
account_number = "1020"
description = "Accounts Receivable"
account_type = "asset"
parent_account_id = "6a1b6a70-6b6e-4a5e-9d2e-6a4f1c7f2a01"
starting_balance_minor_units = 0
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
POST https://api.epsilon3.io/v1/accounting/accounts
Create a new account. Returns the created account with HTTP 201.
| Body parameter | Value |
|---|---|
| account_number | String - required, must be unique within the workspace |
| description | String - required, 1 to 100 characters |
| account_type | String - required, asset | liability | equity | revenue | expense |
| parent_account_id | String - optional, id of an existing account with the same account_type |
| starting_balance_minor_units | Integer - optional, defaults to 0 |
Creating an account with an account_number that already exists returns HTTP 409. The parent account must have the same account_type (HTTP 400 otherwise), and account nesting is limited in depth.
Import Accounts
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/accounting/accounts/import',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
'accounts': [
{
'account_number': '2000',
'description': 'Liabilities',
'account_type': 'liability',
},
{
'account_number': '2010',
'description': 'Accounts Payable',
'account_type': 'liability',
'parent_account_number': '2000',
},
]
}
)
curl -X POST \
https://api.epsilon3.io/v1/accounting/accounts/import \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"accounts": [
{ "account_number": "2000", "description": "Liabilities", "account_type": "liability" },
{ "account_number": "2010", "description": "Accounts Payable", "account_type": "liability", "parent_account_number": "2000" }
]
}'
$apiUrl = "https://api.epsilon3.io/v1/accounting/accounts/import"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
}
$body = @{
accounts = @(
@{ account_number = "2000"; description = "Liabilities"; account_type = "liability" },
@{ account_number = "2010"; description = "Accounts Payable"; account_type = "liability"; parent_account_number = "2000" }
)
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
POST https://api.epsilon3.io/v1/accounting/accounts/import
Create up to 500 accounts in a single request. Rows reference their parent by parent_account_number (an existing account's number, or another row's number in the same request), so a whole hierarchy can be imported at once. The import is all-or-nothing: if any row is invalid, nothing is created and the response is HTTP 400 with a row_errors array describing each failing row. On success, returns the created accounts with HTTP 201.
| Body parameter | Value |
|---|---|
| accounts | Array - required, 1 to 500 rows |
Each row accepts account_number, description, and account_type (all required), plus optional parent_account_number and starting_balance_minor_units.
Update Account
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v1/accounting/accounts/7c2d8b81-7c7f-4b6f-8e3f-7b5a2d8a3b12',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={'description': 'Cash and Cash Equivalents'}
)
curl -X PATCH \
https://api.epsilon3.io/v1/accounting/accounts/7c2d8b81-7c7f-4b6f-8e3f-7b5a2d8a3b12 \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{ "description": "Cash and Cash Equivalents" }'
$apiUrl = "https://api.epsilon3.io/v1/accounting/accounts/7c2d8b81-7c7f-4b6f-8e3f-7b5a2d8a3b12"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
}
$body = @{ description = "Cash and Cash Equivalents" } | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
PATCH https://api.epsilon3.io/v1/accounting/accounts/:accountId
Update an existing account. All body parameters are optional; only supplied fields are changed. Returns the updated account with HTTP 200, or HTTP 404 if the account does not exist.
| Body parameter | Value |
|---|---|
| account_number | String - must remain unique within the workspace (HTTP 409 on conflict) |
| description | String - 1 to 100 characters |
| account_type | String - asset | liability | equity | revenue | expense; only allowed on accounts with no parent or children |
| parent_account_id | String or null - re-parent the account; the parent must have the same account_type, and moves that would create a cycle or exceed the maximum nesting depth are rejected |
| starting_balance_minor_units | Integer |
Delete Accounts
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/accounting/accounts',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={'account_ids': ['7c2d8b81-7c7f-4b6f-8e3f-7b5a2d8a3b12']}
)
curl -X DELETE \
https://api.epsilon3.io/v1/accounting/accounts \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{ "account_ids": ["7c2d8b81-7c7f-4b6f-8e3f-7b5a2d8a3b12"] }'
$apiUrl = "https://api.epsilon3.io/v1/accounting/accounts"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
}
$body = @{ account_ids = @("7c2d8b81-7c7f-4b6f-8e3f-7b5a2d8a3b12") } | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers $headers -Body $body
DELETE https://api.epsilon3.io/v1/accounting/accounts
Deactivate one or more accounts by id. Accounts are soft-deleted: they no longer appear in listings unless include-inactive is set. Returns HTTP 204 on success, or HTTP 404 when none of the ids match an account.
| Body parameter | Value |
|---|---|
| account_ids | Array of String - required, ids of the accounts to delete |
Builds API
The Builds API can be used to manage your part templates (or bills of materials) and list your entire inventory.
Weight Units
Weights are sent and returned as a number together with a weight_unit. The supported unit codes and their exact conversion factors are:
weight_unit |
Unit | Grams per unit |
|---|---|---|
g |
gram | 1 |
kg |
kilogram | 1000 |
oz |
ounce (avoirdupois) | 28.349523125 |
lb |
pound (avoirdupois) | 453.59237 |
- Requests: send a
weighttogether with itsweight_unit. Aweightwithout aweight_unit, or aweight_unitwithout aweight, returns400. - Responses: every object that carries a weight also states its own
weight_unit. By default this is the workspace's small unit:gfor a metric workspace andozfor an imperial one. A workspace without the Weight Units module defaults tog. Pass theweight-unitquery parameter (one ofg,kg,ozorlb) to have weights returned in a specific unit. - Rounding: weights are stored in grams, rounded to 4 decimal places. Weights returned in another unit use the fewest decimal places (4 to 7) that convert back to the same grams, so
2.25lbsent in comes back as2.25, and sending a returned weight back never changes the stored grams. - Deprecated fields: the older
weight_grams,computed_weight_grams,unit_weight_gramsandswap_unit_weight_gramsfields are still accepted and returned, and are always in grams whateverweight-unitis set to. They are deprecated; use the unit-aware fields instead.
Work Orders
Get Work Orders
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/work-orders',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/work-orders \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/work-orders"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/work-orders
Retrieve all work orders defined in your workspace.
Query Parameters
type: (optional) Filter by work order type, eitherAssemblyorService. Repeat the parameter to request more than one type. Defaults toAssembly.linked-run-id: (optional) Only return work orders that the given run is linked to. Repeat the parameter to match any of several runs, for example?linked-run-id=run_123&linked-run-id=run_456.
Example Response Body
{
"work_orders": [
{
"id": "85",
"work_order_number": 69,
"name": "Project 1 WO 1",
"priority": "High",
"status": "Complete",
"owner": "YfsK2SFZ8afndGo2u79ruB",
"notes": "",
"project_id": "9NKegRHwzK",
"parts": [
{
"part_id": "mo58Dbqw4UTxejDt5ZQz71",
"revision_id": "NL1IeyfLXdvVaeWrbqqDg5",
"quantity": 1,
"part_number": "ANODE",
"name": "Anode",
"revision": "1"
}
],
"assignees": [],
"details": [
{
"id": "f2cf0c3a-8a96-409b-95b4-94009b26efc5",
"field_id": "email_id",
"name": "Email",
"type": "Text",
"metadata": null,
"required": false,
"default_value": null,
"expression_tokens": null,
"work_order_id": "85",
"value": {
"recorded": "adam@example.com"
}
},
{
"id": "a4ee7953-cf8e-4bbf-9313-72bfd733ef4c",
"field_id": "avg_cost",
"name": "Average Cost",
"type": "Number",
"metadata": {},
"required": false,
"default_value": null,
"expression_tokens": null,
"work_order_id": "85",
"value": {
"recorded": "312"
}
}
],
"planned_start_date": "2025-07-21T00:00:00.000Z",
"planned_end_date": "2046-07-21T00:00:00.000Z",
"target_date": "2046-08-01T23:59:59.000Z"
},
...
]
}
Get Work Order
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/work-orders/85',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/work-orders/85 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/work-orders/85"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
GET https://api.epsilon3.io/v1/builds/work-orders/:workOrderId
Retrieve a single work order using its ID.
Parameters
workOrderId(path parameter): ID of the work order being retrieved.
Example Response Body
{
"id": "85",
"work_order_number": 69,
"name": "Project 1 WO 1",
"priority": "High",
"status": "Complete",
"owner": "YfsK2SFZ8afndGo2u79ruB",
"project_id": "9NKegRHwzK",
"parts": [
{
"part_id": "mo58Dbqw4UTxejDt5ZQz71",
"revision_id": "NL1IeyfLXdvVaeWrbqqDg5",
"quantity": 1,
"part_number": "ANODE",
"name": "Anode",
"revision": "1"
}
],
"assignees": [],
"details": [
{
"id": "f2cf0c3a-8a96-409b-95b4-94009b26efc5",
"field_id": "email_id",
"name": "Email",
"type": "Text",
"metadata": null,
"required": false,
"default_value": null,
"expression_tokens": null,
"work_order_id": "85",
"value": {
"recorded": "adam@example.com"
}
},
{
"id": "a4ee7953-cf8e-4bbf-9313-72bfd733ef4c",
"field_id": "avg_cost",
"name": "Average Cost",
"type": "Number",
"metadata": {},
"required": false,
"default_value": null,
"expression_tokens": null,
"work_order_id": "85",
"value": {
"recorded": "312"
}
}
],
"planned_start_date": "2025-07-21T00:00:00.000Z",
"planned_end_date": "2046-07-21T00:00:00.000Z",
"target_date": "2046-08-01T23:59:59.000Z",
"ebom_configs": [],
"components_needed": [
{
"part_id": "xi4kPknhkM79WlWdaEXHth",
"revision_id": "3KV7aieAp7jkkE1wZl8WAp",
"quantity": 50,
"part_number": "P-101",
"revision": "B",
"name": "Combustion Chamber",
"available_quantity": 45,
"inventory_assignments": [
{
"item_id": "item_abc",
"quantity": 30
},
{
"item_id": "item_def",
"quantity": 15
}
]
}
],
"work_instructions": [
{
"occurrence_type": "linked",
"procedure_id": "proc_abc",
"procedure_code": "WI-001",
"procedure_name": "Anode assembly",
"sort_order": 0,
"user_id": "YfsK2SFZ8afndGo2u79ruB",
"created_at": "2026-04-10T14:32:11.000Z"
}
],
"linked_runs": [],
"aboms": []
}
Create Work Orders
Sample Request (Single Work Order)
import requests
import os
data = {
"work_orders": [
{
"name": "Build 10x Rocket Engines",
"owner": "jane.doe@example.com",
"parts": [
{
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"quantity": 10
}
]
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/work-orders',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/work-orders \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"work_orders": [
{
"name": "Build 10x Rocket Engines",
"owner": "jane.doe@example.com",
"parts": [
{
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"quantity": 10
}
]
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/work-orders"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
work_orders = @(
@{
name = "Build 10x Rocket Engines"
owner = "jane.doe@example.com"
parts = @(
@{
part_id = "66qVqW9X5NKz7qPJhrlFtG"
revision_id = "W4e4WfDe6ZU8OZdvIaJIUw"
quantity = 10
}
)
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body (Single Work Order)
{
"work_orders": [
{
"name": "Build 10x Rocket Engines",
"owner": "jane.doe@example.com",
"parts": [
{
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"quantity": 10
}
],
"procedures": [
{
"procedure_id": "proc_abc",
"sort_order": 0,
"user_id": "jane.doe@example.com"
}
]
}
]
}
Example Request Body (Multiple Work Orders)
{
"work_orders": [
{
"name": "Build 10x Rocket Engines",
"owner": "jane.doe@example.com",
"parts": [
{
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"quantity": 10
}
]
},
{
"name": "Build 5x Fuel Tanks",
"owner": "john.smith@example.com",
"priority": "High",
"parts": [
{
"part_id": "77qVqW9X5NKz7qPJhrlFtH",
"revision_id": "X5e4WfDe6ZU8OZdvIaJIVx",
"quantity": 5
}
]
}
]
}
Example Response Body (Single Work Order)
{
"ids": ["105"]
}
Example Response Body (Multiple Work Orders)
{
"ids": ["105", "106"]
}
POST https://api.epsilon3.io/v1/builds/work-orders
Create one or more work orders in your workspace. This endpoint supports bulk creation of work orders.
Request Body
work_orders: (required) Array of work order objects to create (1 to 1000 work orders). Each work order object contains:name: (required) Work order name. Must be a string of up to 255 characters.owner: (required) Work order owner identifier. Can be either a user's email address or user ID.parts: (required) Array of parts to build. Must contain exactly 1 part object with:part_id: (required) System generated ID of the part to buildrevision_id: (required) System generated ID of the specific part revisionquantity: (required) Number of parts to build. Must be a positive integer (max 2147483647)priority: (optional) Priority level for the work order. Must be one of:Critical,High,Medium,Lowcustomer_id: (optional) System generated ID of the customerproject_id: (optional) System generated ID of the project. Must be one of the workspace's projects. Returns400for any other id, and no work orders in the request are created.notes: (optional) Work order notes. Must be a string of up to 4096 charactersassignees: (optional) Array of user identifiers for users assigned to the work order (0 to 50 users). Each identifier can be either a user's email address or user IDplanned_start_date: (optional) Planned start date in ISO 8601 formatplanned_end_date: (optional) Planned end date in ISO 8601 formattarget_date: (optional) Target assembly date in ISO 8601 formatform_id: (optional) System generated ID of the form to use for the work order. Determines the layout and which customdetailsfields are available. Defaults to the default form for the work order type.details: (optional) Custom work order detail fields. Must be an array of 0 to 100 objects with:id: (required) ID of the custom fieldvalue: (required) Object containing:recorded: (required) The value to set for the custom field
attachments: (optional) Array of attachment objects (0 to 1000 items). Each object contains:attachment_id: (required) System generated ID of the uploaded attachment.procedures: (optional) Array of procedure objects to link to the work order (0 to 50 items), using the same schema as the Update Work Order Procedures endpoint. When provided, this list is used instead of the work instructions that would otherwise be linked automatically based on the work order's part. Each object contains:procedure_id: (required) System generated ID of a released procedure.sort_order: (required) Non-negative integer that controls display order. Procedures are returned sorted ascending by this value.user_id: (optional) Identifier of the user who owns this work instruction. Can be either a user's email address or user ID.
Response
The response status code will be 200 and return an array of IDs for the created work orders.
Update Work Order
Sample Request
import requests
import os
data = {
"status": "Running"
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/work-orders/105',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/work-orders/105 \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"status": "Running"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/work-orders/105"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
status = "Running"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"status": "Running"
}
PATCH https://api.epsilon3.io/v1/builds/work-orders/:workOrderId
Updates values of one or more fields for a specific work order.
Request Body
The request body should be a JSON object that contains the specific fields of the work order to be updated. The request body must contain at least one field. Following fields are supported:
status: (optional) Work order status. Must be one of:Planned,Running,Complete,Canceledpriority: (optional) Priority level. Must be one of:Critical,High,Medium,Low, ornullto clearcustomer_id: (optional) System generated ID of the customer, ornullto clearproject_id: (optional) System generated ID of the project, ornullto clear. Must be one of the workspace's projects. Returns400for any other id.- Project can only be changed while the work order is in the
Plannedstatus. - Child work orders keep their current project.
- Project can only be changed while the work order is in the
assignees: (optional) Object to manage assignees:added: (optional) Array of user identifiers to add as assignees (0 to 50 users). Each identifier can be either a user's email address or user IDremoved: (optional) Array of user identifiers to remove from assignees (0 to 50 users). Each identifier can be either a user's email address or user ID
details: (optional) Custom work order detail fields to update. Must be an array of 0 to 100 objects with:id: (required) ID of the custom fieldvalue: (required) Object containing:recorded: (required) The value to set for the custom field
ebom_config: (optional) Array of EBOM (Engineering Bill of Materials) configuration updates for work orders with variable components. Must contain exactly 1 configuration object with:id: (required) The EBOM configuration ID to updateselections: (required) Array of component selections (0 to 100 items). Each selection must contain:part_component_group_id: (required) ID of the variable component being configuredpart_component_group_option_id: (required) ID of the selected option for this component group, ornullto indicate no selection if the component is optional
parts: (optional) Update the quantity of the part being built. Must contain exactly 1 part object with:revision_id: (required) System generated ID of the part revision being updated. Must match the part revision already on the work order.quantity: (required) Number of parts to build. Must be a positive integer (max 2147483647)- Quantity can only be edited while the work order is in the
Plannedstatus.
planned_start_date: (optional) Planned start date in ISO 8601 format, ornullto clearplanned_end_date: (optional) Planned end date in ISO 8601 format, ornullto cleartarget_date: (optional) Target assembly date in ISO 8601 format, ornullto clear
Response
- Status:
200 OK - Body: (empty)
Attach Files to Work Order
Sample Request
import requests
import os
with open('traveler.pdf', 'rb') as traveler, open('torque.png', 'rb') as torque:
requests.post(
url='https://api.epsilon3.io/v1/builds/work-orders/105/files',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
files=[('file', traveler), ('file', torque)]
)
curl -X POST \
https://api.epsilon3.io/v1/builds/work-orders/105/files \
-u "$EPSILON3_API_KEY": \
-F "file=@traveler.pdf" \
-F "file=@torque.png"
$apiUrl = "https://api.epsilon3.io/v1/builds/work-orders/105/files"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$form = @{
file = @(Get-Item -Path "traveler.pdf"; Get-Item -Path "torque.png")
}
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
} -Form $form
POST https://api.epsilon3.io/v1/builds/work-orders/:workOrderId/files
Uploads up to 10 files and adds them to the attachments of the specified work order. Existing attachments are kept. Either every file is attached or none is. Attached files are returned in the attachments array of the work order.
Parameters
workOrderId(path parameter): ID of the work order the files are attached to.
Example Response Body
{
"attachments": [
{
"attachment_id": "f48c1df3-dc39-4a1d-90aa-30205f9e8653",
"name": "traveler.pdf",
"content_type": "application/pdf"
},
{
"attachment_id": "0b8d6c1e-6a0f-4d43-9f53-4d1e8a2f7c11",
"name": "torque.png",
"content_type": "image/png"
}
]
}
Request Body
This request should be sent as multipart/form-data containing the following fields:
file: (required) A file to attach. Repeat the field to attach up to 10 files in one request. Each file must not exceed500 MB.attachment_id: (optional) A UUID to store the file under. Send it once per file, in the same order as the files. When omitted, an ID is generated.name: (optional) The attachment name, up to 255 characters. Send it once per file, in the same order as the files. When omitted, the name of the uploaded file is used.
The content type, size and source of each attachment are set from the uploaded file. Sending attachment_id or name a different number of times than file, or an attachment_id that is already in use, returns 422 Unprocessable Entity.
Response
- Status:
201 Created - Body: object with the stored attachments, in upload order:
attachments: Array of attachments, each with:attachment_id: ID of the stored filename: Name of the attachmentcontent_type: MIME type of the file
Delete Work Order
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/builds/work-orders/105',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X DELETE \
https://api.epsilon3.io/v1/builds/work-orders/105 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/work-orders/105"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers @{
"Authorization" = "Basic $authHeader"
}
DELETE https://api.epsilon3.io/v1/builds/work-orders/:workOrderId
Delete a work order.
Parameters
workOrderId(path parameter): ID of the work order to delete
Response
- Status:
200 OK - Body: (empty)
Update Inventory Assignments
Sample Request
import requests
import os
data = {
"added": [
{
"item_id": "Uqo1eK43QThNE78Rl3HTco",
"quantity": 5
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/work-orders/105/inventory-assignments',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/work-orders/105/inventory-assignments \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"added": [
{
"item_id": "Uqo1eK43QThNE78Rl3HTco",
"quantity": 5
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/work-orders/105/inventory-assignments"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
added = @(
@{
item_id = "Uqo1eK43QThNE78Rl3HTco"
quantity = 5
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"added": [
{
"item_id": "Uqo1eK43QThNE78Rl3HTco",
"quantity": 5
}
]
}
POST https://api.epsilon3.io/v1/builds/work-orders/:workOrderId/inventory-assignments
Update inventory assignments for a work order. This endpoint allows you to assign or unassign inventory items to/from a work order.
Request Body
added: (optional) Array of inventory items to assign to the work order (0 to 1000 items). Each object must contain:item_id: (required) System generated ID of the inventory itemquantity: (required) Quantity to assign. Must be a positive number
removed: (optional) Array of inventory items to unassign from the work order (0 to 1000 items). Each object must contain:item_id: (required) System generated ID of the inventory item to remove
Response
- Status:
200 OK - Body: (empty)
Update Work Order Procedures
Sample Request
import requests
import os
data = {
"procedures": [
{
"procedure_id": "proc_abc",
"sort_order": 0,
"user_id": "YfsK2SFZ8afndGo2u79ruB"
},
{
"procedure_id": "proc_def",
"sort_order": 1
}
]
}
requests.put(
url='https://api.epsilon3.io/v1/builds/work-orders/105/procedures',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PUT \
https://api.epsilon3.io/v1/builds/work-orders/105/procedures \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"procedures": [
{
"procedure_id": "proc_abc",
"sort_order": 0,
"user_id": "YfsK2SFZ8afndGo2u79ruB"
},
{
"procedure_id": "proc_def",
"sort_order": 1
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/work-orders/105/procedures"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
procedures = @(
@{
procedure_id = "proc_abc"
sort_order = 0
user_id = "YfsK2SFZ8afndGo2u79ruB"
},
@{
procedure_id = "proc_def"
sort_order = 1
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Put -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"procedures": [
{
"procedure_id": "proc_abc",
"sort_order": 0,
"user_id": "YfsK2SFZ8afndGo2u79ruB"
},
{
"procedure_id": "proc_def",
"sort_order": 1
}
]
}
PUT https://api.epsilon3.io/v1/builds/work-orders/:workOrderId/procedures
Replace the full set of procedures linked to a work order. Procedures present in the request but not currently linked are added; procedures currently linked but absent from the request are unlinked. Use this endpoint to add, remove, reorder, or assign owners to the work instructions on a work order.
Parameters
workOrderId(path parameter): ID of the work order whose procedures are being updated.
Request Body
procedures: (required) Array of procedure objects to link to the work order (0 to 50 items). Pass an empty array to unlink all procedures. Each object contains:procedure_id: (required) System generated ID of a released procedure.sort_order: (required) Non-negative integer that controls display order. Procedures are returned sorted ascending by this value.user_id: (optional) Identifier of the user who owns this work instruction. Can be either a user's email address or user ID.
Response
- Status:
200 OK - Body: (empty)
Link Runs to Work Order
Sample Request
import requests
import os
data = {
"run_ids": ["run_123"]
}
requests.put(
url='https://api.epsilon3.io/v1/builds/work-orders/105/runs',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PUT \
https://api.epsilon3.io/v1/builds/work-orders/105/runs \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"run_ids": ["run_123"]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/work-orders/105/runs"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
run_ids = @("run_123")
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Put -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"run_ids": ["run_123"]
}
PUT https://api.epsilon3.io/v1/builds/work-orders/:workOrderId/runs
Link procedure runs to a work order. This associates completed or in-progress runs with the work order for tracking purposes.
Request Body
run_ids: (required) Array of run IDs to link to the work order. Must contain at least 1 and at most 50 run IDs.
A run can only be linked to one work order at a time. Linking a run that already belongs to a different work order returns 400. Linking a run to a Planned work order moves that work order to Running.
Response
- Status:
200 OK - Body: (empty)
Unlink Run from Work Order
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/builds/work-orders/105/runs/run_123',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X DELETE \
https://api.epsilon3.io/v1/builds/work-orders/105/runs/run_123 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/work-orders/105/runs/run_123"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers @{
"Authorization" = "Basic $authHeader"
}
DELETE https://api.epsilon3.io/v1/builds/work-orders/:workOrderId/runs/:runId
Unlink a run from a work order. The run itself is not modified, it is only no longer tracked by the work order, so ending the work order will no longer end the run.
The work order must be Planned or Running, and the run must currently be linked to it. Otherwise the request returns 400.
Response
- Status:
200 OK - Body: (empty)
List Work Order Activity
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/work-orders/109/activity',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/work-orders/109/activity \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/work-orders/109/activity"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
Example Response Body
{
"has_older": false,
"activities": [
{
"id": "98",
"work_order_id": "109",
"timestamp": "2024-10-18T20:02:38.901Z",
"action": "Status Updated",
"actor_type": "user",
"actor_id": "user@epsilon3.io",
"old_value": {
"status": "Planned"
},
"new_value": {
"status": "Running"
}
},
{
"id": "97",
"work_order_id": "109",
"timestamp": "2024-10-18T19:45:12.123Z",
"action": "Assignees Updated",
"actor_type": "user",
"actor_id": "admin@epsilon3.io",
"old_value": {
"assignee_ids": ["user1@epsilon3.io"]
},
"new_value": {
"assignee_ids": ["user1@epsilon3.io", "user2@epsilon3.io"]
}
},
{
"id": "96",
"work_order_id": "109",
"timestamp": "2024-10-18T19:30:05.456Z",
"action": "Quantity Updated",
"actor_type": "user",
"actor_id": "user@epsilon3.io",
"old_value": {
"quantity": 2
},
"new_value": {
"quantity": 5
}
}
]
}
GET https://api.epsilon3.io/v1/builds/work-orders/:workOrderId/activity?startBeforeId={startBeforeId}
List up to twenty five most recent actions related to the specified work order. Use the optional startBeforeId query parameter to list older actions.
Parameters
workOrderId(path parameter): ID of the work order for which to list activitystartBeforeId(query parameter): optional integer that can be used to list older activity. When this is set, up to twenty five most recent actions that are older than the activity mapped to this ID are listed.
Parts
Get Parts
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/parts',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/parts \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/parts"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/parts
Retrieve parts defined in your workspace. The response is paginated and can be filtered and sorted using the query parameters below.
Example Response Body
{
"parts": [
{
"id": "vnWtKWleNWiuSQbBPNdE5F",
"part_number": "P-1",
"name": "Sample Part",
"revision": "C",
"tracking": "serial",
"description": "This is a sample part assembled in house.",
"assembly": false,
"procurement_type": "buy",
"allow_decimal_quantities": false,
"units": null,
"project_id": "9NKegRHwzK",
"form_id": "6f1d2c3b-4a5e-4f60-8a9b-0c1d2e3f4a5b",
"unit_price_cents": 20000,
"low_inventory_threshold": 10,
"created_at": "2023-07-18T14:49:08.713Z",
"updated_at": "2024-02-03T09:12:45.120Z"
},
...
],
"pagination": {
"limit": 1000,
"total": 1342,
"next_page_token": "eyJrZXkiOnsidmFsdWUiOiJQLTEwMDAiLCJpZCI6IlFrN21UejNSd1h5QjJoTGNWbjlwRGEifSwiZGlyZWN0aW9uIjoibmV4dCIsImxpbWl0IjoxMDAwLCJzb3J0QnkiOiJwYXJ0X251bWJlciIsInNvcnRPcmRlciI6ImFzYyIsImdyb3VwQnkiOm51bGwsImZpbHRlcnMiOm51bGx9"
}
}
Paging Parameters
limit(query parameter): optional maximum number of parts to return per page. Must be an integer between1and1000. Defaults to1000when omitted from the first page.pageToken(query parameter): optional continuation token. To fetch the next page, pass back thenext_page_tokenreturned in the previous response'spaginationblock. Use the token only as returned — do not construct or parse it. A token picks up after the last part of the previous page, so parts created, changed, or deleted while you page do not cause other parts to be skipped. A part changed while you page may be returned more than once, or, when sorting bypart_numberor byupdated_atdescending, skipped. To be sure every existing part is returned, sort bycreated_at, or byupdated_atascending. Restart paging from the first page when you change the filters,sort,order, orlimit; a token used with different filters or a differentsort,order, orlimitis rejected with a 400. Whenlimitis omitted from a later page, the first page's limit is used.sort(query parameter): optional field to sort by. Must be one ofpart_number,created_at, orupdated_at. Defaults topart_number.order(query parameter): optional sort direction. Must be eitherascordesc. Defaults toasc. Results have a stable secondary sort by part ID.
Filter Parameters
Each filter may be specified multiple times to match against multiple values. Within a single filter the values are combined with OR; different filters are combined with AND.
partId(query parameter): optional part ID(s) to filter to specific parts.partNumber(query parameter): optional part number(s) to filter to specific parts. Matching is case-insensitive.projectId(query parameter): optional project ID(s) to return only parts scoped to those projects.procurementType(query parameter): optional procurement type(s). Each value must be one ofbuy,make, ormake or buy.tracking(query parameter): optional tracking type(s). Each value must be one ofserial,lot, ornone.tag(query parameter): optional tag ID(s) to return only parts associated with those tags.preferredVendor(query parameter): optional vendor ID(s) to return only parts whose preferred vendor matches.assembly(query parameter): optional boolean. Set totrueto return only assemblies, orfalseto return only non-assembly parts.includeUnreleased(query parameter): optional boolean. Must be eithertrueorfalse. Whentrue, parts whose latest revision has not been released are included (and a part with no released revision is represented by its latest revision of any state). Defaults tofalse.detail_{field_id}(query parameter): optional. Filter by a part-level custom field with the givenfield_id. See Custom field filters.
Timestamp Parameters
Each bound is an ISO 8601 timestamp in extended format, such as 2026-09-01T00:00:00.000Z, and is inclusive. A date without a time, such as 2026-09-01, means midnight UTC at the start of that day, so createdAtBefore=2026-09-01 does not include parts created later on September 1. To include the whole day, use createdAtBefore=2026-09-01T23:59:59.999Z. Unlike the filter parameters, each timestamp parameter may be specified at most once.
createdAtAfter(query parameter): optional. Return only parts created at or after this time.createdAtBefore(query parameter): optional. Return only parts created at or before this time.updatedAtAfter(query parameter): optional. Return only parts changed at or after this time.updatedAtBefore(query parameter): optional. Return only parts changed at or before this time.
A part's updated_at changes whenever the part or any of its revisions changes. That includes its fields, components, component groups, custom field values, connection interfaces, usage declarations, release state, reviewers, image, schematic, and attachments. It also changes when a part or revision it uses as a component is deleted. It does not change when a custom field or usage type definition is changed (for example renamed or removed), when a component part is edited, or when another assembly starts or stops using the part (see includeUsedIn). Tagging a part or changing its preferred vendor does not change updated_at either, so combining tag or preferredVendor with updatedAtAfter does not return a part that only newly matches those filters. Deleted parts drop out of the list and are not reported by these filters. Parts not edited since updated_at was introduced report the same updated_at as created_at.
Query parameter example usage
GET https://api.epsilon3.io/v1/builds/parts?detail_material=Aluminum&detail_mass:gte=10
GET https://api.epsilon3.io/v1/builds/parts?updatedAtAfter=2026-09-01T00:00:00.000Z&sort=updated_at
Response fields
pagination: Paging metadata for the response.limit: The page size that was applied.total: Total number of parts matching the filters across all pages.next_page_token: Continuation token for retrieving the next page, ornullwhen this is the last page.
created_at: ISO 8601 timestamp of when the part was created.updated_at: ISO 8601 timestamp of when the part last changed. See Timestamp Parameters above for what counts as a change.unit_price_cents: Unit price of the part in U.S. cents (integer). Included only when set on the part.low_inventory_threshold: Quantity at or below which the part is considered low inventory (integer). Included only when set on the part.project_id: System generated ID of the project the part belongs to. Included only when the part is scoped to a project.form_id: System generated ID of the form the part uses. Determines which customdetailsfields are available on the part.
Custom field filters on parts
This endpoint accepts dynamic query parameters that filter results by the value of a part-level custom field. The parameter name encodes both the field and an optional operator.
detail_{field_id}=valuefilters by the part's custom field. Thefield_idis the stable identifier returned by list custom fields.- An operator suffix may be appended after a colon:
detail_{field_id}:{op}=value. When the suffix is omitted, the operator defaults toeq. - Repeating the same key creates an OR-set of accepted operands for that operator (e.g.,
detail_material=Aluminum&detail_material=Steelmatches parts whosematerialisAluminumorSteel). - The empty string
""is a sentinel that matches "no value recorded".
Supported operators per custom field type:
| Field type | Allowed operators |
|---|---|
Number, Date, Time, Date Time |
eq, ne, gt, gte, lt, lte |
Text |
eq, ne, contains |
List, Custom List, Checkbox |
eq, ne |
External Data |
None — External Data fields cannot be filtered |
Operand format per type: ISO-8601 strings for Date / Time / Date Time, numeric strings for Number, plain strings for Text / List / Custom List, true or false for Checkbox.
contains matches Text values that include the case insensitive operand anywhere, does not accept empty strings or wildcards.
For List fields, an operand matches either the selected option's display text or its option_id (see Recorded Custom Field Values). Filtering by option_id keeps matching after an option is renamed. Values recorded without an option_id can only be matched by their text.
Get Part
GET https://api.epsilon3.io/v1/builds/parts/:partId?revisionId={revisionId}
Retrieve a single part using its ID.
Parameters
partId(path parameter): ID of the part being retrieved.revisionId(query parameter): optional ID used to retrieve a specific part revisionincludeComponentTree(query parameter): optional boolean. Whentrue, each entry incomponentscarries its own nestedcomponentsarray, giving the full multi-level BOM instead of only the part's direct components.includeUsedIn(query parameter): optional boolean. Whentrue, the response includestop_level_parent_boms: the assemblies this part rolls up into, each as a full BOM tree rooted at a top-level parent. Every entry uses the same shape as an entry incomponents. Anullentry is a parent whose tree could not be built because of a cycle in the component graph. The field is omitted entirely when the parameter is absent.
Example Request URL
GET https://api.epsilon3.io/v1/builds/parts/vnWtKWleNWiuSQbBPNdE5F
Example Response Body
{
"id": "vnWtKWleNWiuSQbBPNdE5F",
"revision": "C",
"part_number": "P-1",
"name": "Sample Part",
"description": "This is a sample part assembled in house.",
"tracking": "serial",
"allow_decimal_quantities": false,
"units": null,
"unit_price_cents": 20000,
"low_inventory_threshold": 10,
"project_id": "9NKegRHwzK",
"form_id": "6f1d2c3b-4a5e-4f60-8a9b-0c1d2e3f4a5b",
"created_at": "2023-07-18T14:49:08.713Z",
"updated_at": "2024-02-03T09:12:45.120Z",
"assembly": false,
"procurement_type": "make",
"usage_declarations": [
{
"id": 10,
"name": "Run Hours",
"team_id": "k8f2n5q9w3x7z1c4v6b8m2p5r7t9y3h6",
"kind": "accumulate",
"units": "hours",
"precision": 1,
"allow_negative": false,
"allow_reset": true
}
],
"usage_limits": [
{
"usage_type_id": 10,
"limit_max": 2000,
"limit_warn_max": 1800,
"enforcement": "block"
}
],
"shelf_life_minutes": 262800,
"shelf_life_warn_minutes": 43200,
"expiry_enforcement": "warn",
"details": [
{
"id": "xagtLhLxMEPLTgEHDNiEBa",
"name": "External Id",
"value": "123"
}
],
"revisions": [
{
"revision": "C",
"id": "W4e4WfDe6ZU8OZdvIaJIUw",
"release_state": "released"
},
{
"revision": "B",
"id": "W4e4WfDe6ZU8OZdvIaJIUw",
"release_state": "released"
},
{
"revision": "A",
"id": "aIv8S4kVCWuHui7Dax7Smy",
"release_state": "released"
}
],
"components": [
{
"part_id": "xi4kPknhkM79WlWdaEXHth",
"quantity": 2,
"revision": "B",
"revision_id": "3KV7aieAp7jkkE1wZl8WAp",
"ref_des_groups": [
{
"id": "8ZN9YkkLs7T8L52gqRRG6V",
"ref_des": ["R1", "R2"],
"quantity": 2
}
]
}
]
}
Response fields
usage_declarations: the usage types the revision declares, in the same shape as List Usage Types. See Usage.usage_limits: the revision's usage limits, each withusage_type_id,enforcement, and whichever oflimit_min,limit_maxandlimit_warn_maxare set. Declared types without a limit are left out.shelf_life_minutesandshelf_life_warn_minutes: the revision's shelf life, ornullwhen it has none.expiry_enforcement:allow,warnorblock.
Create Parts
Request URL
POST https://api.epsilon3.io/v1/builds/parts
Example Request Body
{
"parts": [
{
"part_number": "P-1",
"name": "Sample Part",
"revision": "C",
"tracking": "serial",
"description": "This is a sample part assembled in house.",
"assembly": false,
"project_id": "9NKegRHwzK",
"details": [
{
"id": "xagtLhLxMEPLTgEHDNiEBa",
"value": { "recorded": "123" }
}
]
},
...
]
}
Example Response Body
{
"parts": [
{
"id": "vnWtKWleNWiuSQbBPNdE5F",
"part_number": "P-1",
"name": "Sample Part",
"revision": "C",
"tracking": "serial",
"description": "This is a sample part assembled in house.",
"allow_decimal_quantities": false,
"units": null,
"unit_price_cents": 20000,
"low_inventory_threshold": 10,
"project_id": "9NKegRHwzK",
"form_id": "6f1d2c3b-4a5e-4f60-8a9b-0c1d2e3f4a5b",
"created_at": "2023-07-18T14:49:08.713Z",
"updated_at": "2023-07-18T14:49:08.713Z",
"assembly": false,
"procurement_type": "buy",
"details": [
{
"id": "xagtLhLxMEPLTgEHDNiEBa",
"name": "External Id",
"value": "123"
}
],
"revisions": [
{
"revision": "C",
"id": "MkeDh0hepepkhKbczKGP0H",
"release_state": "released"
}
],
"components": []
},
...
]
}
Example request body with a component part
{
"parts": [
{
"part_number": "P-2",
"name": "Sample Part with components",
"revision": "A",
"tracking": "serial",
"description": "This is a sample part assembled in house.",
"assembly": false,
"procurement_type": "make",
"components": [
{
"part_number": "P-1",
"revision": "C",
"quantity": 2,
"ref_des_groups": [
{
"ref_des": ["R1", "R2"],
"quantity": 2
}
]
}
]
},
...
]
}
Example Request Body with different custom field value types
{
"details": [
{
"id": "1N0QAyYTU5nY7PWVXvaF84", // Text field
"value": { "recorded": "Example text value" }
},
{
"id": "02768b6e-29cc-4c26-94a1-32eef6173a0f", // Number field
"value": { "recorded": 456 }
},
{
"id": "2bcaa4b6-c062-4955-9ad6-9f128089753f", // Checkbox field
"value": { "recorded": true }
},
{
"id": "fbf9d2f0-3ba2-430e-a686-d93335edd036", // Date Time field
"value": { "recorded": "2025-12-25T14:30:00.000Z" }
},
{
"id": "8761daca-66b5-4179-a21d-eeb12f140461", // Date field
"value": { "recorded": "2025-12-25" }
},
{
"id": "42b329ee-4c3a-468b-893b-af635d39f97e", // Custom List field ('Damaged' is an option in the Custom List)
"value": { "recorded": "Damaged" }
},
{
"id": "f5c7d3d2-ca1d-4851-b9e8-6e63e8bc5ea0", // Time field
"value": { "recorded": {"time": "11:12:11", "zone": "US/Pacific"} }
},
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", // Unset field with null value
"value": { "recorded": null }
}
]
}
Example Response Body
{
"parts": [
{
"id": "FMtBcwA5fYZwV4xOJwlRdZ",
"part_number": "P-2",
"name": "Sample Part with components",
"revision": "A",
"tracking": "serial",
"description": "This is a sample part assembled in house.",
"allow_decimal_quantities": false,
"units": null,
"unit_price_cents": 20000,
"low_inventory_threshold": 10,
"created_at": "2023-07-18T14:49:08.713Z",
"updated_at": "2023-07-18T14:49:08.713Z",
"assembly": false,
"procurement_type": "make",
"components": [
{
"part_id": "vnWtKWleNWiuSQbBPNdE5F",
"revision": "C",
"revision_id": "MkeDh0hepepkhKbczKGP0H",
"quantity": 2,
"ref_des_groups": [
{
"id": "8ZN9YkkLs7T8L52gqRRG6V",
"ref_des": ["R1", "R2"],
"quantity": 2
}
]
}
],
"revisions": [
{
"revision": "A",
"id": "k7jaxluqyThfl7PLBIkCbI",
"release_state": "released"
}
],
"details": []
},
...
]
}
POST https://api.epsilon3.io/v1/builds/parts
Add one or more parts to your workspace.
Request Body
parts: an array of objects, each containing the following set of fields
part_number: (required) Part Number or Identifier, unique across all parts in the workspace. Must be a string of up to 64 characters.revision: (required) Revision Identifier. Must be a string of up to 64 characters.name: (required) Part Name. Must be a string of up to 128 characters.tracking: (required) Tracking type used for part. Must be one ofserial,lot, ornone.description: (optional) Part description. Must be a string of up to 512 characters.assembly: (optional) Mark part as assembly. Must be a boolean value.procurement_type: (optional) Mark part as 'buy', 'make', or 'make or buy'. Defaults to 'buy'.allow_decimal_quantities: (optional) Set totrueto allow inventory items for this part to have decimal quantities (up to four significant digits).- Ignored for
serialtracked parts
- Ignored for
units: (optional) Units to use for this part. Must be a string of up to 255 characters.- To set to a unit already defined in application settings, use the
namevalue for the unit
- To set to a unit already defined in application settings, use the
unit_price_cents: (optional) Unit price of the part in U.S. cents. Must be a non-negative integer.project_id: (optional) ID of the project to assign the part to.form_id: (optional) System generated ID of the form to use for the part. Determines which customdetailsfields are available. Must be one of the workspace's Part forms (GET /v1/forms?entity=Part), archived forms included; defaults to the default Part form. Returns400for any other id. The form cannot be changed through Update Part.components: (optional) Part components. Must be an array of objects with the following format:part_number: (required) Part Number of the component partrevision: (required) Revision identifier of the component partquantity: (required) Number of component parts. Must be a positive integerref_des_groups: (optional) Reference designator groups for this component, used to identify install locations on the parent assembly. Must be an array of up to 500 objects with the following format:ref_des: (required) Array of reference designator labels (e.g.,["R1", "R2"]). Must contain 1 to 500 non-empty strings, each up to 128 characters.quantity: (required) Number of installs covered by this group. Must be a non-negative number.- Each group is assigned a server-generated
idand thatidis returned in the response. Do not includeidwhen creating a group.
details: (optional) Custom part detail fields to set on the part. Must be an array of objects with the following format:id: (required) ID of the part detail being setvalue: (required) Object containing:recorded: (required) The value to set for the part detail (supports strings, numbers, booleans, objects, or null). Set to null to unset
usage_declaration_ids: (optional) Array of 0 to 50 IDs of usage types the part declares, so its inventory items can record usage of them. See Usage.- Archived usage types cannot be declared.
usage_limits: (optional) Array of 0 to 50 life limits on declared usage types. Each object has:usage_type_id: (required) ID of a usage type the part declares.limit_max: (optional) For anaccumulatetype, the total since the latest reset at which an item is at its limit. For asnapshottype, the highest reading an item may have.limit_warn_max: (optional)accumulatetypes only. The total at which an item is near its limit. Must be belowlimit_max.limit_min: (optional)snapshottypes only. The lowest reading an item may have. Cannot be abovelimit_max.enforcement: (optional) What happens when an item at its limit is used, for example in a run:allow,warn(the use goes ahead with a warning) orblock(the use is refused). Defaults towarn.categoricaltypes cannot take limits. A limit that breaks these rules, or names a usage type the part does not declare, returns400.
shelf_life_minutes: (optional) How long the part's inventory items last, in whole minutes from 1 to 52,560,000 (100 years). An item added without anexpires_atexpires this long after it is added.shelf_life_warn_minutes: (optional) How many minutes before it expires an item counts as expiring soon, from 1 to 52,560,000. Requiresshelf_life_minutesand must be less than it; otherwise the request returns400.expiry_enforcement: (optional) What happens when an expired item is used:allow,warnorblock. Defaults towarn.
Usage declarations, usage limits and shelf life belong to the part revision being created.
Response
The response status code will be 200 and return a list of the created parts.
Update Part
Example Request Body
{
"name": "Sample Part with components",
"revision": "B",
"tracking": "serial",
"description": "This is a sample part assembled in house.",
"assembly": true,
"procurement_type": "make",
"components": [
{
"part_number": "P-1",
"quantity": 2,
"revision": "C",
"ref_des_groups": [
{
"ref_des": ["R1", "R2"],
"quantity": 2
}
]
}
],
"details": [
{
"id": "xagtLhLxMEPLTgEHDNiEBa",
"value": "123"
}
]
}
Example Request Body with different custom field value types
{
"details": [
{
"id": "1N0QAyYTU5nY7PWVXvaF84", // Text field
"value": { "recorded": "Example text value" }
},
{
"id": "02768b6e-29cc-4c26-94a1-32eef6173a0f", // Number field
"value": { "recorded": 456 }
},
{
"id": "2bcaa4b6-c062-4955-9ad6-9f128089753f", // Checkbox field
"value": { "recorded": true }
},
{
"id": "fbf9d2f0-3ba2-430e-a686-d93335edd036", // Date Time field
"value": { "recorded": "2025-12-25T14:30:00.000Z" }
},
{
"id": "8761daca-66b5-4179-a21d-eeb12f140461", // Date field
"value": { "recorded": "2025-12-25" }
},
{
"id": "42b329ee-4c3a-468b-893b-af635d39f97e", // Custom List field ('Damaged' is an option in the Custom List)
"value": { "recorded": "Damaged" }
},
{
"id": "f5c7d3d2-ca1d-4851-b9e8-6e63e8bc5ea0", // Time field
"value": { "recorded": {"time": "11:12:11", "zone": "US/Pacific"} }
},
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", // Unset field with null value
"value": { "recorded": null }
}
]
}
Example Response Body
{
"id": "FMtBcwA5fYZwV4xOJwlRdZ",
"revision": "B",
"part_number": "P-2",
"name": "Sample Part with components",
"tracking": "serial",
"description": "This is a sample part assembled in house.",
"allow_decimal_quantities": false,
"units": null,
"unit_price_cents": 20000,
"low_inventory_threshold": 10,
"created_at": "2023-07-18T14:49:08.713Z",
"updated_at": "2024-02-03T09:12:45.120Z",
"assembly": true,
"procurement_type": "make",
"components": [
{
"part_id": "vnWtKWleNWiuSQbBPNdE5F",
"revision": "C",
"revision_id": "MkeDh0hepepkhKbczKGP0H",
"quantity": 2,
"ref_des_groups": [
{
"id": "8ZN9YkkLs7T8L52gqRRG6V",
"ref_des": ["R1", "R2"],
"quantity": 2
}
]
}
]
"details": [
{
"id": "xagtLhLxMEPLTgEHDNiEBa",
"name": "External Id",
"value": "123"
}
],
"revisions": [
{
"revision": "B",
"id": "W4e4WfDe6ZU8OZdvIaJIUw",
"release_state": "released"
},
{
"revision": "A",
"id": "k7jaxluqyThfl7PLBIkCbI",
"release_state": "released"
}
],
}
PATCH https://api.epsilon3.io/v1/builds/parts/:partId
Updates values of one or more fields for a specific part.
Request Body
The request body should be a JSON object that contains the specific fields of the part to be updated. The request body must contain at least one field. Following fields are supported:
part_number: (optional) Part Number or Identifier. Must be unique across all parts in the workspace. Must be a string of up to 64 characters.revision: (optional) Revision Identifier. Must be a string of up to 64 characters.- Updating the value of this field creates a new revision for the part.
name: (optional) Part Name. Must be a string of up to 128 characters.tracking: (optional) Tracking type used for part. Must be one ofserial,lot, ornone.description: (optional) Part description. Must be a string of up to 512 characters.allow_decimal_quantities: (optional) Set totrueto allow inventory items for this part to have decimal quantities (up to four significant digits).- Ignored for
serialtracked parts
- Ignored for
units: (optional) Units to use for this part. Must be a string of up to 255 characters.- Set to
nullto clear - To set to a unit already defined in application settings, use the
namevalue for the unit
- Set to
unit_price_cents: (optional) Unit price of the part in U.S. cents. Must be a non-negative integer.project_id: (optional) ID of the project to assign the part to.- Set to
nullto clear
- Set to
assembly: (optional) Mark part as assembly. Must be a boolean value.procurement_type: (optional) Mark part as 'buy', 'make', or 'make or buy'. Defaults to 'buy'.components: (optional) Part components.- Must be an array of objects with the following fields:
part_number: (required) Part Number of the component partrevision: (required) Revision identifier of the component partquantity: (required) Number of component parts. Must be a positive integerref_des_groups: (optional) Reference designator groups for this component, used to identify install locations on the parent assembly. Must be an array of up to 500 objects with the following format:ref_des: (required) Array of reference designator labels (e.g.,["R1", "R2"]). Must contain 1 to 500 non-empty strings, each up to 128 characters.quantity: (required) Number of installs covered by this group. Must be a non-negative number.
- Each group is assigned a server-generated
idand thatidis returned in the response. Do not includeidwhen updating components. - The array provided in this request will replace the existing array of components. Specify an empty array to remove all components from a part.
- Component parts must already exist.
details: (optional) Custom part detail fields to set or unset on the part.- Must be an array of objects with the following format:
id: (required) ID of the part detail being setvalue:recorded: (required) The value to set for the part detail (supports strings, numbers, booleans, objects, or null). Set to null to unset
- Details already set on the part, but not provided in this array, will not be affected
usage_declaration_ids: (optional) Array of 0 to 50 IDs of usage types the part declares, so its inventory items can record usage of them. See Usage.- The array replaces the part's declarations. Specify an empty array to remove them all; leave the field out to keep them.
- Removing a declaration also removes its usage limit.
- Archived usage types cannot be newly declared.
usage_limits: (optional) Array of 0 to 50 life limits on declared usage types, in the same format as on Create Parts.- The array replaces the part's limits: a declared usage type left out loses its limit. Specify an empty array to remove them all; leave the field out to keep them.
- A limit can name a usage type declared in the same request.
shelf_life_minutes: (optional) How long the part's inventory items last, in whole minutes from 1 to 52,560,000 (100 years), ornullto clear. To clear a shelf life that has a warning, also send"shelf_life_warn_minutes": nullin the same request; otherwise the request returns400. Changing it does not change items'expires_atunlessupdate_item_expirationsis set.shelf_life_warn_minutes: (optional) How many minutes before it expires an item counts as expiring soon, from 1 to 52,560,000, ornullto clear. It must be less than the shelf life, and cannot be set without one; otherwise the request returns400.expiry_enforcement: (optional) What happens when an expired item is used:allow,warnorblock.update_item_expirations: (optional) Set totrueto resetexpires_aton every item of the revision to the time the item was added plus the revision's shelf life after this update. This overwrites expirations set by hand, and clears them when the revision has no shelf life. It has no effect when the update creates a new revision.
Usage declarations, usage limits and shelf life belong to a part revision. When an update creates a new revision, the new revision starts with the previous revision's values, and the request's changes apply to the new revision.
Update Part Image
PUT https://api.epsilon3.io/v1/builds/parts/:partId/image
Updates the primary image of the specified part with the provided file. The image is only updated for the latest revision of the part.
Request Body
This request should be sent as multipart/form-data containing the following fields:
name: (required) Name of the file. Must be a string of up to 255 characters.file: (required) The image file. File size must not exceed500 MB.
Response Body
- Status:
200 OK - Body: (empty)
Attach File to Part
POST https://api.epsilon3.io/v1/builds/parts/:partId/attachments
Attaches the provided file to the specified part. The file is only attached to the latest revision of the part.
Request Body
This request should be sent as multipart/form-data containing the following fields:
name: (required) Name of the file. Must be a string of up to 255 characters.file: (required) The file being attached. File size must not exceed500 MB.
Response Body
- Status:
200 OK - Body: (empty)
Attach Files to Part
Sample Request
import requests
import os
with open('traveler.pdf', 'rb') as traveler, open('torque.png', 'rb') as torque:
requests.post(
url='https://api.epsilon3.io/v1/builds/parts/66qVqW9X5NKz7qPJhrlFtG/files',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
files=[('file', traveler), ('file', torque)]
)
curl -X POST \
https://api.epsilon3.io/v1/builds/parts/66qVqW9X5NKz7qPJhrlFtG/files \
-u "$EPSILON3_API_KEY": \
-F "file=@traveler.pdf" \
-F "file=@torque.png"
$apiUrl = "https://api.epsilon3.io/v1/builds/parts/66qVqW9X5NKz7qPJhrlFtG/files"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$form = @{
file = @(Get-Item -Path "traveler.pdf"; Get-Item -Path "torque.png")
}
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
} -Form $form
POST https://api.epsilon3.io/v1/builds/parts/:partId/files
Uploads up to 10 files and adds them to the attachments of the specified part. Existing attachments are kept. Either every file is attached or none is. The files are attached to the latest revision of the part. Returns 409 while another user holds the draft lock of that revision. Unlike Attach File to Part, several files can be attached in one request, and each file keeps the name it was uploaded with.
Parameters
partId(path parameter): ID of the part the files are attached to.
Example Response Body
{
"attachments": [
{
"attachment_id": "f48c1df3-dc39-4a1d-90aa-30205f9e8653",
"name": "traveler.pdf",
"content_type": "application/pdf"
},
{
"attachment_id": "0b8d6c1e-6a0f-4d43-9f53-4d1e8a2f7c11",
"name": "torque.png",
"content_type": "image/png"
}
]
}
Request Body
This request should be sent as multipart/form-data containing the following fields:
file: (required) A file to attach. Repeat the field to attach up to 10 files in one request. Each file must not exceed500 MB.attachment_id: (optional) A UUID to store the file under. Send it once per file, in the same order as the files. When omitted, an ID is generated.name: (optional) The attachment name, up to 255 characters. Send it once per file, in the same order as the files. When omitted, the name of the uploaded file is used.
The content type, size and source of each attachment are set from the uploaded file. Sending attachment_id or name a different number of times than file, or an attachment_id that is already in use, returns 422 Unprocessable Entity.
Response
- Status:
201 Created - Body: object with the stored attachments, in upload order:
attachments: Array of attachments, each with:attachment_id: ID of the stored filename: Name of the attachmentcontent_type: MIME type of the file
Update Part Model / Schematic
Example Response Body
{
"attachment_id": "9ANPjB6cSCe2PgHDWWCJ2N",
"name": "assembly.step",
"content_type": "application/octet-stream",
"size": 148230
}
PUT https://api.epsilon3.io/v1/builds/parts/:partId/schematic
Updates the model / schematic of the specified part with the provided file. The model / schematic is only updated for the latest revision of the part. Any existing model / schematic and its reference designator annotations are replaced.
Request Body
This request should be sent as multipart/form-data containing the following fields:
name: (required) Name of the file. Must be a string of up to 255 characters, and must end in a supported CAD extension (.step,.stp,.iges,.igs,.stl,.obj,.3mf) unless the file is an image.file: (required) The model / schematic file. Supported types are CAD models (STEP, IGES, STL, OBJ, 3MF) and images. File size must not exceed500 MB.
Response Body
- Status:
200 OK - Body: object with the saved model / schematic attachment:
attachment_id: System-generated ID of the stored filename: Name of the filecontent_type: MIME type of the filesize: File size in bytes
Delete Part Model / Schematic
DELETE https://api.epsilon3.io/v1/builds/parts/:partId/schematic
Removes the model / schematic and its reference designator annotations from the latest revision of the specified part.
Response Body
- Status:
200 OK - Body: (empty)
Delete Part
Request Body
{
"ids": array (string)
}
Example Request Body
{
"ids": ["SRHRaJV1qBiYZdQ3Tm7P6C", "vnWtKWleNWiuSQbBPNdE5F"]
}
Example Response Body
{
"deleted_part_ids": ["SRHRaJV1qBiYZdQ3Tm7P6C"],
"invalid_part_ids": ["vnWtKWleNWiuSQbBPNdE5F"]
}
DELETE https://api.epsilon3.io/v1/builds/parts
Bulk deletes parts using provided array of part ids. Deleting a part will also delete its associated revisions and items. NOTE: Deleted parts will be permanently deleted.
Part Details
List Part Details
GET https://api.epsilon3.io/v1/builds/part-details
Retrieve all custom fields that can be set on a part
Request URL
GET https://api.epsilon3.io/v1/builds/part-details
Example Response Body
{
"part_details": [
{
"id": "xagtLhLxMEPLTgEHDNiEBa",
"name": "External Id"
}
]
}
Inventory
List Inventory
Request URL
GET https://api.epsilon3.io/v1/builds/inventory
Example Response Body
{
"items": [
{
"id": "Uqo1eK43QThNE78Rl3HTco",
"part_id": "vnWtKWleNWiuSQbBPNdE5F",
"part_revision_id": "MkeDh0hepepkhKbczKGP0H",
"tracking_id": "P1-004",
"quantity": 1,
"abom_quantity": 1,
"location_id": 23,
"unit_cost_cents": 20000,
"computed_unit_cost_cents": 18500,
"weight": 250.5,
"computed_weight": 245.0,
"weight_unit": "g",
"weight_grams": 250.5,
"computed_weight_grams": 245.0,
"notes": "Passed all quality checks!",
"components": [
{
"item_id": "f4yEjyK8PQNkdRy6EvncFy",
"amount": 1,
"ref_des_group": {
"id": "8ZN9YkkLs7T8L52gqRRG6V",
"labels": ["R1"]
}
}
],
"status": "In Inventory",
"created_at": "2023-07-18T14:49:08.713Z"
},
...
],
"pagination": {
"limit": 1000,
"total": 1342,
"next_page_token": "eyJvZmZzZXQiOjEwMDB9"
}
}
GET https://api.epsilon3.io/v1/builds/inventory
Retrieve inventory items defined in your workspace. The response is paginated and can be filtered and sorted using the query parameters below.
Paging Parameters
limit(query parameter): optional maximum number of items to return per page. Must be an integer between1and1000. Defaults to1000when omitted.pageToken(query parameter): optional continuation token. To fetch the next page, pass back thenext_page_tokenreturned in the previous response'spaginationblock. Use the token only as returned — do not construct or parse it. Changing the filters or sort invalidates a previously returned token; restart paging from the first page when they change.sort(query parameter): optional field to sort by. Must be one ofcreated_atortracking_id. Defaults tocreated_at.order(query parameter): optional sort direction. Must be eitherascordesc. Defaults todesc. Results have a stable secondary sort by item ID.
Filter Parameters
Each filter may be specified multiple times to match against multiple values. Within a single filter the values are combined with OR; different filters are combined with AND.
partId(query parameter): optional part ID(s) to retrieve items only for specific parts.trackingId(query parameter): optional tracking ID(s) used to filter retrieved items to matching tracking IDs.status(query parameter): optional inventory status(es) to filter by. Each value must be one ofIn Inventory,Unavailable,Assigned,Consumed,Checked Out,Kitted,Rejected,Sold,Shipped, orScrap. When omitted, items of all statuses are returned.tracking(query parameter): optional part tracking type(s). Each value must be one ofserial,lot, ornone. This filters by the tracking type of the item's part, distinct fromtrackingId.projectId(query parameter): optional project ID(s) to return only items whose part is scoped to those projects.locationId(query parameter): optional location ID(s) to return only items at those locations. Use the valuenoneto match items that have no location assigned.includeConsumed(query parameter): optional parameter that can be set to indicate whether to include items with quantity value of 0. Must be eithertrueorfalse. Defaults totrue.
Unit Parameters
weight-unit(query parameter): optional unit for the returnedweightandcomputed_weight. Must be one ofg,kg,ozorlb. Defaults to the workspace's small unit. See Weight Units.
Response fields
pagination: Paging metadata for the response.limit: The page size that was applied.total: Total number of items matching the filters across all pages.next_page_token: Continuation token for retrieving the next page, ornullwhen this is the last page.
Get Inventory Item
GET https://api.epsilon3.io/v1/builds/inventory/:itemId
Retrieve a specific inventory item using its ID.
Parameters
itemId(path parameter): ID of the item being retrieved.includeUsedIn(query parameter): optional boolean. Whentrue, the response includeslinked_aboms: the as-built assemblies this item is installed in, each as a tree of{ item_id, amount, components }rooted at a top-level parent item. The field is omitted entirely when the parameter is absent.weight-unit(query parameter): optional unit for the returnedweightandcomputed_weight. Must be one ofg,kg,ozorlb. Defaults to the workspace's small unit. See Weight Units.
Example Request URL
GET https://api.epsilon3.io/v1/builds/inventory/Uqo1eK43QThNE78Rl3HTco
Example Response Body
{
"id": "Uqo1eK43QThNE78Rl3HTco",
"part_id": "vnWtKWleNWiuSQbBPNdE5F",
"part_revision_id": "MkeDh0hepepkhKbczKGP0H",
"tracking_id": "P1-004",
"quantity": 1,
"abom_quantity": 1,
"location_id": 23,
"unit_cost_cents": 20000,
"computed_unit_cost_cents": 18500,
"weight": 250.5,
"computed_weight": 245.0,
"weight_unit": "g",
"weight_grams": 250.5,
"computed_weight_grams": 245.0,
"notes": "Passed all quality checks!",
"components": [
{
"item_id": "f4yEjyK8PQNkdRy6EvncFy",
"amount": 1,
"ref_des_group": {
"id": "8ZN9YkkLs7T8L52gqRRG6V",
"labels": ["R1"]
}
}
],
"status": "In Inventory",
"expires_at": "2024-07-18T14:49:08.713Z",
"usage_declarations": [
{
"id": 10,
"name": "Run Hours",
"team_id": "k8f2n5q9w3x7z1c4v6b8m2p5r7t9y3h6",
"kind": "accumulate",
"units": "hours",
"precision": 1,
"allow_negative": false,
"allow_reset": true
}
],
"usage_summaries": [
{
"usage_type_id": 10,
"usage_type_name": "Run Hours",
"usage_type_kind": "accumulate",
"usage_type_units": "hours",
"usage_type_precision": 1,
"latest_recorded_at": "2024-02-03T09:12:45.120Z",
"reading_count": 14,
"latest_value": 12.5,
"total_value": 1840.5,
"reading_count_since_reset": 14
}
],
"usage_limit_count": 1,
"usage_limit_alerts": [
{
"usage_type_id": 10,
"usage_type_name": "Run Hours",
"usage_type_kind": "accumulate",
"usage_type_units": "hours",
"usage_type_precision": 1,
"limit_max": 2000,
"limit_warn_max": 1800,
"enforcement": "block",
"state": "near",
"value": 1840.5
}
],
"created_at": "2023-07-18T14:49:08.713Z"
}
Response fields
status: Current inventory status of the item. One ofIn Inventory,Unavailable,Assigned,Consumed,Checked Out,Kitted,Rejected,Sold,Shipped, orScrap.created_at: ISO 8601 timestamp of when the item was created.weight: The item's own weight, inweight_unit. Omitted when the item has no weight.computed_weight: The item's weight computed from its components, inweight_unit. Omitted when it cannot be computed.weight_unit: The unit ofweightandcomputed_weight. See Weight Units.weight_grams: Deprecated. The item's own weight in grams. Useweightinstead.computed_weight_grams: Deprecated. The computed weight in grams. Usecomputed_weightinstead.components: The items installed into this item.linked_aboms: The assemblies this item is installed in. Only present whenincludeUsedIn=true.expires_at: ISO 8601 timestamp of when the item expires. Omitted when it has no expiration.usage_declarations: The usage types the item's part revision declares, which the item can record. Same shape as List Usage Types.usage_summaries: The item's current usage, one entry per usage type it has readings for. Same shape as List Usage Summaries, without the subject ID.usage_limit_count: The number of usage limits set on the item's part revision. Omitted when it has none.usage_limit_alerts: The usage limits the item is near or at. Omitted when there are none. Each alert has the limit'susage_type_id,enforcementand bounds, the usage type'susage_type_name,usage_type_kindand, when set,usage_type_unitsandusage_type_precision, and:state:nearonce anaccumulatetotal reacheslimit_warn_max, oratonce it reacheslimit_maxor asnapshotreading falls outsidelimit_minandlimit_max.value: the total or reading that raised the alert.
Add Inventory
Example Request Body
{
"items": [
{
"part_id": "T7bpWix6bs86tEoCqIk26n",
"part_revision_id": "m4ssiRUzCcN9mJUKjhvHvA",
"tracking_id": "TP00001",
"quantity": 1,
"location_id": 23,
"unit_cost_cents": 50000,
"weight": 125.75,
"weight_unit": "g",
"notes": "All inspections passed!"
},
...
]
}
Example curl request to create inventory item with custom fields
curl -X POST \
-H 'Content-Type: application/json' \
-u ${EPSILON3_API_KEY}: \
-d '{
"items": [{
"part_id": "SRHRaJV1qBiYZdQ3Tm7P6C",
"tracking_id": "API_TEST_1",
"quantity": 1,
"details": [
{
"id": "fu0shg9uk3eYCeG3ctuAde",
"value": {"recorded": "Example text"}
},
{
"id": "c1b031d9-33b0-40de-9eb1-893708882862",
"value": {"recorded": 99}
},
{
"id": "51d70499-3876-4415-a453-d47b6688046c",
"value": {"recorded": "Damaged"}
},
{
"id": "fbf9d2f0-3ba2-430e-a686-d93335edd036",
"value": {"recorded": "2025-12-25T14:30:00.000Z"}
},
{
"id": "2bcaa4b6-c062-4955-9ad6-9f128089753f",
"value": {"recorded": true}
}
]
}]
}' \
https://api.epsilon3.io/v1/builds/inventory
Example Response Body
{
"items": [
{
"id": "f4yEjyK8PQNkdRy6EvncFy",
"part_id": "T7bpWix6bs86tEoCqIk26n",
"part_revision_id": "m4ssiRUzCcN9mJUKjhvHvA",
"tracking_id": "TP00001",
"quantity": 1,
"abom_quantity": 1,
"location_id": 23,
"unit_cost_cents": 50000,
"weight": 125.75,
"weight_unit": "g",
"weight_grams": 125.75,
"notes": "Passed all quality checks!",
"status": "In Inventory",
"created_at": "2023-12-17T01:33:16.996Z"
},
...
]
}
POST https://api.epsilon3.io/v1/builds/inventory
Adds new items to inventory.
Query Parameters
weight-unit: (optional) Unit for the weights in the response. Must be one ofg,kg,ozorlb. Defaults to the workspace's small unit.
Request Body
items: (required) An array of objects with the following fieldspart_id: (required) System generated ID of the part of which this item is an instance. Must be a string.part_revision_id: (optional) System generated ID for a specific revision of the part. If not specified, the latest part revision is used to create the item. Must be a string.tracking_id: (required) Serial or Lot # of the item if it is a tracked item. Blank string if it is an untracked item. Must be a string of up to 64 characters.quantity: (required) Item count. Must be a non negative integer. Count can only be either0or1for serial tracked items.location_id: (optional) System generated ID of the specific location where this item should be located.unit_cost_cents: (optional) Unit cost of item in US cents. Must be a non negative integer.weight: (optional) Weight of the item, inweight_unit. Must be a non negative number. Requiresweight_unit.weight_unit: (optional) Unit ofweight. Must be one ofg,kg,ozorlb. Required whenweightis set. See Weight Units.weight_grams: (optional, deprecated) Weight of item in grams. Must be a non negative number. Useweightandweight_unitinstead. Sending bothweightandweight_gramsreturns400.notes: (optional) Any notes to attach to the item. Notes are displayed in the Epsilon3 Builds UI with support for Markdown. Must be a string of up to 4096 characters.details: (optional) Custom inventory item detail fields to set on the item. Must be an array of objects with the following format:id: (required) ID of the inventory item detail being setvalue: (required) Value to set for the inventory item detail. For custom fields, use the format{"recorded": actualValue}whereactualValuecan be a string, number, boolean, date, or objectexpires_at: (optional) When the item expires. Must be an ISO 8601 date-time with a time zone offset, such as2026-12-31T00:00:00Z, ornullfor no expiration. When left out, the item expires the part revision'sshelf_life_minutesafter it is added, or never when the revision has no shelf life.
Update Inventory Item
Example Request Body
{
"quantity": 1,
"abom_quantity": 1,
"location_id": 23,
"unit_cost_cents": 20000,
"weight": 250.5,
"weight_unit": "g",
"notes": "Passed all quality checks!",
"details": [
{
"id": "xagtLhLxMEPLTgEHDNiEBa",
"value": "Updated detail value"
}
]
}
Example Request Body to set different custom field value types
{
"details": [
{
"id": "1N0QAyYTU5nY7PWVXvaF84", // Text field
"value": "Example text value"
},
{
"id": "02768b6e-29cc-4c26-94a1-32eef6173a0f", // Number field
"value": 456
},
{
"id": "2bcaa4b6-c062-4955-9ad6-9f128089753f", // Checkbox field
"value": true
},
{
"id": "fbf9d2f0-3ba2-430e-a686-d93335edd036", // Date Time field
"value": "2025-12-25T14:30:00.000Z"
},
{
"id": "8761daca-66b5-4179-a21d-eeb12f140461", // Date field
"value": "2025-12-25"
},
{
"id": "42b329ee-4c3a-468b-893b-af635d39f97e", // Custom List field
"value": "Damaged"
},
{
"id": "f5c7d3d2-ca1d-4851-b9e8-6e63e8bc5ea0", // Time field
"value": {"time": "11:12:11", "zone": "US/Pacific"}
},
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", // Unset field with null value
"value": null
}
]
}
Example Response Body
{
"id": "Uqo1eK43QThNE78Rl3HTco",
"part_id": "vnWtKWleNWiuSQbBPNdE5F",
"part_revision_id": "MkeDh0hepepkhKbczKGP0H",
"tracking_id": "P1-004",
"quantity": 1,
"abom_quantity": 1,
"location_id": 23,
"unit_cost_cents": 20000,
"weight": 250.5,
"weight_unit": "g",
"weight_grams": 250.5,
"notes": "Passed all quality checks!",
"status": "In Inventory",
"created_at": "2023-07-18T14:49:08.713Z"
}
Example curl request to update inventory item custom fields
curl -X PATCH \
-H 'Content-Type: application/json' \
-u ${EPSILON3_API_KEY}: \
-d '{
"details": [
{
"id": "fu0shg9uk3eYCeG3ctuAde",
"value": {"recorded": "Example text"}
},
{
"id": "c1b031d9-33b0-40de-9eb1-893708882862",
"value": {"recorded": 99}
},
{
"id": "51d70499-3876-4415-a453-d47b6688046c",
"value": {"recorded": "Damaged"}
},
{
"id": "fbf9d2f0-3ba2-430e-a686-d93335edd036",
"value": {"recorded": "2025-12-25T14:30:00.000Z"}
},
{
"id": "2bcaa4b6-c062-4955-9ad6-9f128089753f",
"value": {"recorded": true}
}
]
}' \
https://api.epsilon3.io/v1/builds/inventory/:itemId
Example Response Body
{
"id": "Uqo1eK43QThNE78Rl3HTco",
"part_id": "vnWtKWleNWiuSQbBPNdE5F",
"part_revision_id": "MkeDh0hepepkhKbczKGP0H",
"tracking_id": "P1-004",
"quantity": 1,
"abom_quantity": 1,
"location_id": 23,
"unit_cost_cents": 20000,
"weight": 250.5,
"weight_unit": "g",
"weight_grams": 250.5,
"notes": "Passed all quality checks!",
"details": [
{
"id": "fu0shg9uk3eYCeG3ctuAde",
"name": "Text Field",
"value": {"recorded": "Example text"}
},
{
"id": "c1b031d9-33b0-40de-9eb1-893708882862",
"name": "Number Field",
"value": {"recorded": 99}
},
{
"id": "51d70499-3876-4415-a453-d47b6688046c",
"name": "Status Field",
"value": {"recorded": "Damaged"}
}
],
"status": "In Inventory",
"created_at": "2023-07-18T14:49:08.713Z"
}
PATCH https://api.epsilon3.io/v1/builds/inventory/:itemId
Updates values of one or more fields for a specific item.
Query Parameters
weight-unit: (optional) Unit for the weights in the response. Must be one ofg,kg,ozorlb. Defaults to the workspace's small unit.
Request Body
The request body should be a JSON object that contains the specific fields of the item to be updated. The request body must contain at least one field. Following fields are supported:
quantity: (optional) Must be a non-negative integer. For serial tracked items, must be either0or1.abom_quantity: (optional) As Built Quantity used to scale component calculations on the As Built. Must be a non-negative number. For serial tracked items, must be1.location_id: (optional) Location ID mapped to the location. Set tonullto remove.unit_cost_cents: (optional) Unit Cost of item in U.S. cents. Set tonullto remove.weight: (optional) Weight of the item, inweight_unit. Must be a non negative number. Requiresweight_unit. Set tonullto remove.weight_unit: (optional) Unit ofweight. Must be one ofg,kg,ozorlb. Required whenweightis a number. See Weight Units.weight_grams: (optional, deprecated) Weight of item in grams. Must be a non negative number. Set tonullto remove. Useweightandweight_unitinstead. Sending bothweightandweight_gramsreturns400.notes: (optional) Free text notes to attach to item. Must be a string of up to 4096 characters.details: (optional) Custom inventory item detail fields to update. Must be an array of 0 to 500 objects with:id: (required) ID of the custom fieldvalue: (required) The value to set for the custom field. For custom fields, use the format{"recorded": actualValue}whereactualValuecan be a string, number, boolean, date, or object. Set tonullto unset the field
expires_at: (optional) When the item expires. Must be an ISO 8601 date-time with a time zone offset, such as2026-12-31T00:00:00Z. Set tonullto remove the expiration.
Delete Inventory
DELETE https://api.epsilon3.io/v1/builds/inventory
Bulk deletes inventory items using provided array of item ids.
An item referenced by build history (weight log, TIR, or connections) cannot be deleted. If any requested item is, no items are deleted and the request returns 409.
Request Body
{
"ids": array (string),
}
Example Request Body
{
"ids": ["SRHRaJV1qBiYZdQ3Tm7P6C", "vnWtKWleNWiuSQbBPNdE5F"]
}
Example Response Body
{
"deleted_item_ids": ["SRHRaJV1qBiYZdQ3Tm7P6C"],
"invalid_item_ids": ["vnWtKWleNWiuSQbBPNdE5F"]
}
List Inventory Item Activity
Example Request URL
GET https://api.epsilon3.io/v1/builds/inventory/6nNJuH7hga4HYCNOL7htbb/activity?startBeforeId=99
Example Response Body
{
"has_older": false,
"activities": [
{
"id": "98",
"item_id": "6nNJuH7hga4HYCNOL7htbb",
"timestamp": "2024-10-18T20:02:38.901Z",
"action": "Created",
"actor_type": "user",
"actor_id": "user@epsilon3.io",
"old_value": null,
"new_value": {
"amount": 1,
"status": "In Inventory",
"location_id": 23
}
}
]
}
GET https://api.epsilon3.io/v1/builds/inventory/:itemId/activity?startBeforeId={startBeforeId}
List up to twenty five most recent actions related to the specified inventory item. Use the optional startBeforeId query parameter to list older actions.
Parameters
itemId(path parameter): ID of the item for which to list activitystartBeforeId(query parameter): optional integer that can be used to list older activity. When this is set, up to twenty five most recent actions that are older than the activity mapped to this ID are listed.
Update Inventory Item Usage
POST https://api.epsilon3.io/v1/builds/inventory/:itemId/usage
Updates usage for an inventory item. (Note: currently only reset is supported.)
Request Body
{
"action": "reset",
"usage_type": string
}
Sample Request
{
"action": "reset",
"usage_type": "cycles"
}
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/builds/inventory/:itemId/usage',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"action": "reset",
"usage_type": "cycles",
}
)
curl -X POST \
https://api.epsilon3.io/v1/builds/inventory/:itemId/usage \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"action": "reset",
"usage_type": "cycles",
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/inventory/:itemId/usage"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"action": "reset",
"usage_type": "cycles",
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers
Response
The response status code will be 200 on successful reset.
Scrap Inventory
Example Request Body
{
"items": [
{
"item_id": "f4yEjyK8PQNkdRy6EvncFy",
"quantity": 3
},
...
]
}
Example curl request to scrap inventory items
curl -X POST \
-H 'Content-Type: application/json' \
-u ${EPSILON3_API_KEY}: \
-d '{
"items": [
{ "item_id": "f4yEjyK8PQNkdRy6EvncFy", "quantity": 3 }
]
}' \
https://api.epsilon3.io/v1/builds/scrap-inventory
POST https://api.epsilon3.io/v1/builds/scrap-inventory
Scraps a quantity of one or more inventory items. Lot-tracked and untracked items have their available quantity decremented, becoming unavailable when it reaches zero. Serial tracked items are marked as scrap and must be scrapped with a quantity of 1. Each scrapped item is recorded in the item's activity log.
Request Body
items: (required) A non-empty array of objects with the following fieldsitem_id: (required) System generated ID of the inventory item to scrap. Must be a string. Item IDs must be unique within the request.quantity: (required) Quantity to scrap. Must be a positive number that does not exceed the item's available quantity. For serial tracked items, it must be1.
Response
The response status code will be 204 on a successful scrap.
Purchase Orders
List Purchase Orders
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/orders',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/orders \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/orders"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/orders
Retrieve all purchase orders in your workspace.
Example Response Body
{
"purchase_orders": [
{
"id": "hCga2bVz70tAXnjhHbvczA",
"code": "PO-1",
"state": "submitted",
"vendor": {
"id": "4kGbI42h7cIo0Ai3dsC9m3",
"name": "Acme Engines Ltd",
"address": "1428 Satellite Loop\nAnytown USA\n10101",
"contact": "elmer@acme.test",
"notes": "Brick & Mortar are our specialties"
},
"ship_to_name": "Headquarters",
"ship_to_address": "123 Alpha Way\nAnytown USA\n10101",
"delivery_date": "07/01/2024",
"notes": "Rush order",
"created_by": "YfsK2SFZ8afndGo2u79ruB",
"lines": [
{
"id": "9Mb0pP3RvjGcQzKfXyYZ12",
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"part": {
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"part_number": "ENGINE-1",
"revision": "C",
"name": "Rocket Engine"
},
"unit_amount": 8000,
"quantity": 1
}
],
"attachments": [],
"details": [],
"created_at": "2024-06-19T02:06:14.204Z",
"updated_at": "2024-06-19T02:06:14.204Z"
}
]
}
Get Purchase Order
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
GET https://api.epsilon3.io/v1/builds/orders/:purchaseOrderId
Retrieve a single purchase order using its ID.
Parameters
purchaseOrderId(path parameter): 22-character ID of the purchase order being retrieved.
Example Response Body
{
"id": "hCga2bVz70tAXnjhHbvczA",
"code": "PO-1",
"state": "submitted",
"vendor": {
"id": "4kGbI42h7cIo0Ai3dsC9m3",
"name": "Acme Engines Ltd",
"address": "1428 Satellite Loop\nAnytown USA\n10101",
"contact": "elmer@acme.test",
"notes": "Brick & Mortar are our specialties",
"payment_terms": "Net 30",
"terms_and_conditions": null,
"discount_bp": null
},
"ship_to_name": "Headquarters",
"ship_to_address": "123 Alpha Way\nAnytown USA\n10101",
"delivery_date": "07/01/2024",
"sales_tax_basis_points": 825,
"discount_bp": 250,
"notes": "Rush order",
"created_by": "YfsK2SFZ8afndGo2u79ruB",
"lines": [
{
"id": "9Mb0pP3RvjGcQzKfXyYZ12",
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"part": {
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"part_number": "ENGINE-1",
"revision": "C",
"name": "Rocket Engine"
},
"unit_amount": 8000,
"quantity": 1,
"details": [
{
"id": "f2cf0c3a-8a96-409b-95b4-94009b26efc5",
"value": {
"recorded": "Expedited"
}
}
]
}
],
"attachments": [
{
"attachment_id": "att_abc123"
}
],
"details": [
{
"id": "a4ee7953-cf8e-4bbf-9313-72bfd733ef4c",
"value": {
"recorded": "PO-Q3-2024"
}
}
],
"work_orders": [
{
"id": "85",
"work_order_number": 69,
"name": "Project 1 WO 1"
}
],
"created_at": "2024-06-19T02:06:14.204Z",
"updated_at": "2024-06-20T18:30:22.512Z",
"submitted_date": "2024-06-20T18:30:22.512Z"
}
Create Purchase Orders
Sample Request (Single Purchase Order)
import requests
import os
data = {
"purchase_orders": [
{
"vendor_id": "4kGbI42h7cIo0Ai3dsC9m3",
"ship_to_name": "Headquarters",
"ship_to_address": "123 Alpha Way\nAnytown USA\n10101",
"delivery_date": "07/01/2024",
"notes": "Rush order",
"created_by": "jane.doe@example.com",
"lines": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"description": "Rocket Engine",
"unit_amount": 8000,
"quantity": 1
}
]
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/orders',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/orders \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"purchase_orders": [
{
"vendor_id": "4kGbI42h7cIo0Ai3dsC9m3",
"ship_to_name": "Headquarters",
"ship_to_address": "123 Alpha Way\nAnytown USA\n10101",
"delivery_date": "07/01/2024",
"notes": "Rush order",
"created_by": "jane.doe@example.com",
"lines": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"description": "Rocket Engine",
"unit_amount": 8000,
"quantity": 1
}
]
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/orders"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
purchase_orders = @(
@{
vendor_id = "4kGbI42h7cIo0Ai3dsC9m3"
ship_to_name = "Headquarters"
ship_to_address = "123 Alpha Way`nAnytown USA`n10101"
delivery_date = "07/01/2024"
notes = "Rush order"
created_by = "jane.doe@example.com"
lines = @(
@{
part_revision_id = "W4e4WfDe6ZU8OZdvIaJIUw"
description = "Rocket Engine"
unit_amount = 8000
quantity = 1
}
)
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
POST https://api.epsilon3.io/v1/builds/orders
Create one or more purchase orders in your workspace. This endpoint supports bulk creation of purchase orders. The system-generated code (e.g. PO-1) and id are returned in the response and cannot be supplied in the request body.
Example Request Body (Single Purchase Order)
{
"purchase_orders": [
{
"vendor_id": "4kGbI42h7cIo0Ai3dsC9m3",
"ship_to_name": "Headquarters",
"ship_to_address": "123 Alpha Way\nAnytown USA\n10101",
"delivery_date": "07/01/2024",
"notes": "Rush order",
"created_by": "jane.doe@example.com",
"lines": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"description": "Rocket Engine",
"unit_amount": 8000,
"quantity": 1
}
]
}
]
}
Example Request Body (Multiple Purchase Orders)
{
"purchase_orders": [
{
"vendor_id": "4kGbI42h7cIo0Ai3dsC9m3",
"created_by": "jane.doe@example.com",
"lines": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"unit_amount": 8000,
"quantity": 1
}
]
},
{
"vendor_id": "5lHcJ53i8dJp1Bj4etD0n4",
"created_by": "john.smith@example.com",
"discount_bp": 250,
"lines": [
{
"part_revision_id": "X5e4WfDe6ZU8OZdvIaJIVx",
"description": "Fuel Tank",
"unit_amount": 1200,
"quantity": 5
}
]
}
]
}
Example Response Body
{
"ids": ["hCga2bVz70tAXnjhHbvczA"]
}
Request Body
purchase_orders: (required) Array of purchase order objects to create (1 to 1000 purchase orders). Each purchase order object contains:created_by: (required) Identifier of the user creating the purchase order. Can be either a user's email address or user ID.order_number: (optional) Purchase order number, displayed asPO-{order_number}. Must be a positive integer that is not already used by another purchase order in the workspace, and must be unique within the request. If omitted, the next available number is assigned.vendor_id: (optional) System generated ID of the vendor for this purchase order.ship_to_name: (optional) Ship-to recipient name.ship_to_address: (optional) Ship-to address. Must be a string of up to 4096 characters.delivery_date: (optional) Requested delivery date inMM/DD/YYYYformat.sales_tax_basis_points: (optional) Sales tax expressed in basis points (100 bp = 1%). Must be a non-negative integer.tax_jurisdiction_id: (optional) UUID of the tax jurisdiction to apply at the PO level.discount_bp: (optional) Discount expressed in basis points (100 bp = 1%). Must be an integer between 1 and 10000.notes: (optional) Free-form notes. Must be a string of up to 4096 characters.work_order_id: (optional) System generated ID of a work order to link this purchase order to.lines: (optional) Array of purchase order line objects (0 to 1000 items). Each line contains:unit_amount: (required) Per-unit cost. Must be a number.quantity: (required) Quantity ordered. Must be a non-negative number.part_revision_id: (optional) System generated ID of the specific part revision being ordered.description: (optional) Free-form description of the line item.tax_jurisdiction_id: (optional) UUID of the tax jurisdiction to apply at the line level. Overrides the PO-level jurisdiction.details: (optional) Custom line-level detail fields. Must be an array of 0 to 100 objects with:id: (required) ID of the custom field.value: (required) Object containing:recorded: (required) The value to set for the custom field.
details: (optional) Custom purchase order detail fields. Must be an array of 0 to 100 objects with:id: (required) ID of the custom field.value: (required) Object containing:recorded: (required) The value to set for the custom field.
attachments: (optional) Array of attachment objects (0 to 1000 items). Each object contains:attachment_id: (required) System generated ID of the uploaded attachment.
Response
- Status:
201 Created - Body: object with
ids, an array of system-generated 22-character IDs for the created purchase orders, in the same order as the request.
Update Purchase Order
Sample Request
import requests
import os
data = {
"notes": "Updated delivery instructions"
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"notes": "Updated delivery instructions"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
notes = "Updated delivery instructions"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"notes": "Updated delivery instructions"
}
PATCH https://api.epsilon3.io/v1/builds/orders/:purchaseOrderId
Updates values of one or more fields on a specific purchase order. State and line items have their own dedicated endpoints and cannot be changed here.
Parameters
purchaseOrderId(path parameter): 22-character ID of the purchase order being updated.
Request Body
The request body is a JSON object containing the fields to be updated. Any field may be passed as null to clear its current value.
vendor_id: (optional) System generated ID of the vendor, ornullto clear.ship_to_name: (optional) Ship-to recipient name, ornullto clear.ship_to_address: (optional) Ship-to address (max 4096 characters), ornullto clear.delivery_date: (optional) Requested delivery date inMM/DD/YYYYformat, ornullto clear.sales_tax_basis_points: (optional) Sales tax in basis points (non-negative integer), ornullto clear.tax_jurisdiction_id: (optional) UUID of the tax jurisdiction to apply at the PO level, ornullto clear.discount_bp: (optional) Discount in basis points (integer between 1 and 10000), ornullto clear.notes: (optional) Notes (max 4096 characters), ornullto clear.work_order_id: (optional) System generated ID of a work order to link this purchase order to.details: (optional) Custom purchase order detail fields to set. Must be an array of 0 to 100 objects with:id: (required) ID of the custom field.value: (required) Object containing:recorded: (required) The value to set for the custom field.
attachments: (optional) Array of attachment objects (0 to 1000 items). Each object contains:attachment_id: (required) System generated ID of the uploaded attachment.
Response
- Status:
200 OK - Body: the full updated
PurchaseOrderobject (same shape asGET /v1/builds/orders/:purchaseOrderId).
Update Purchase Order Lines
Sample Request
import requests
import os
data = {
"added": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"description": "Rocket Engine",
"unit_amount": 8000,
"quantity": 1
}
],
"updated": [
{
"id": "9Mb0pP3RvjGcQzKfXyYZ12",
"quantity": 2
}
],
"removed": [
{
"id": "0Nc1qQ4SwkHdRzLgYzZA23"
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/order-lines',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/order-lines \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"added": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"description": "Rocket Engine",
"unit_amount": 8000,
"quantity": 1
}
],
"updated": [
{
"id": "9Mb0pP3RvjGcQzKfXyYZ12",
"quantity": 2
}
],
"removed": [
{
"id": "0Nc1qQ4SwkHdRzLgYzZA23"
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/order-lines"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
added = @(
@{
part_revision_id = "W4e4WfDe6ZU8OZdvIaJIUw"
description = "Rocket Engine"
unit_amount = 8000
quantity = 1
}
)
updated = @(
@{
id = "9Mb0pP3RvjGcQzKfXyYZ12"
quantity = 2
}
)
removed = @(
@{
id = "0Nc1qQ4SwkHdRzLgYzZA23"
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"added": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"description": "Rocket Engine",
"unit_amount": 8000,
"quantity": 1
}
],
"updated": [
{
"id": "9Mb0pP3RvjGcQzKfXyYZ12",
"quantity": 2
}
],
"removed": [
{
"id": "0Nc1qQ4SwkHdRzLgYzZA23"
}
]
}
POST https://api.epsilon3.io/v1/builds/orders/:purchaseOrderId/order-lines
Apply a diff of changes to a purchase order's line items. This endpoint is not a replacement of all line items — only the lines referenced in added, updated, and removed are affected. Lines not mentioned are left unchanged. All three arrays are optional; if all three are empty, the endpoint is a no-op.
Parameters
purchaseOrderId(path parameter): 22-character ID of the purchase order whose lines are being updated.
Request Body
added: (optional) Array of new line items to append to the purchase order (0 to 1000 items). Each object contains:unit_amount: (required) Per-unit cost. Must be a number.quantity: (required) Quantity ordered. Must be a non-negative number.part_revision_id: (optional) System generated ID of the part revision being ordered.description: (optional) Free-form description of the line item.tax_jurisdiction_id: (optional) UUID of the tax jurisdiction to apply at the line level.details: (optional) Custom line-level detail fields (0 to 100 items). Each object contains:id: (required) ID of the custom field.value: (required) Object containing:recorded: (required) The value to set for the custom field.
updated: (optional) Array of existing line items to modify (0 to 1000 items). Each object contains:id: (required) ID of the existing line to update.part_revision_id: (optional) System generated ID of the part revision, ornullto clear.description: (optional) Free-form description, ornullto clear.unit_amount: (optional) Per-unit cost.quantity: (optional) Quantity ordered.tax_jurisdiction_id: (optional) UUID of the tax jurisdiction, ornullto clear.details: (optional) Custom line-level detail fields (0 to 100 items). Same shape as inadded.
removed: (optional) Array of existing line items to delete (0 to 1000 items). Each object contains:id: (required) ID of the existing line to remove.
Response
- Status:
204 No Content - Body: (empty)
Update Purchase Order State
Sample Request
import requests
import os
data = {
"order_state": "submitted"
}
requests.put(
url='https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/state',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PUT \
https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/state \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"order_state": "submitted"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/state"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
order_state = "submitted"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Put -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"order_state": "submitted"
}
PUT https://api.epsilon3.io/v1/builds/orders/:purchaseOrderId/state
Transition a purchase order to a new state. This is the only endpoint that may change a purchase order's state.
Parameters
purchaseOrderId(path parameter): 22-character ID of the purchase order whose state is being changed.
Request Body
order_state: (required) New state for the purchase order. Must be one of:draft,submitted,closed,canceled,in_review,partially_received,fully_received.
Response
- Status:
200 OK - Body: the full updated
PurchaseOrderobject (same shape asGET /v1/builds/orders/:purchaseOrderId).
Delete Purchase Order
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X DELETE \
https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers @{
"Authorization" = "Basic $authHeader"
}
DELETE https://api.epsilon3.io/v1/builds/orders/:purchaseOrderId
Delete a purchase order.
Parameters
purchaseOrderId(path parameter): 22-character ID of the purchase order to delete.
Response
- Status:
204 No Content - Body: (empty)
List Purchase Order Activity
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/activity',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/activity \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/activity"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
Example Response Body
{
"has_older": false,
"activities": [
{
"id": "201",
"entity_id": "hCga2bVz70tAXnjhHbvczA",
"timestamp": "2024-06-20T18:30:22.512Z",
"action": "Status Updated",
"actor_type": "user",
"actor_id": "user@epsilon3.io",
"old_value": {
"state": "draft"
},
"new_value": {
"state": "submitted"
}
},
{
"id": "200",
"entity_id": "hCga2bVz70tAXnjhHbvczA",
"timestamp": "2024-06-19T02:06:14.204Z",
"action": "Created",
"actor_type": "user",
"actor_id": "user@epsilon3.io"
}
]
}
GET https://api.epsilon3.io/v1/builds/orders/:purchaseOrderId/activity?startBeforeTimestamp={startBeforeTimestamp}
List up to twenty five most recent actions related to the specified purchase order. Use the optional startBeforeTimestamp query parameter to page through older activity.
Parameters
purchaseOrderId(path parameter): 22-character ID of the purchase order for which to list activity.startBeforeTimestamp(query parameter): optional ISO 8601 timestamp. When set, up to twenty five most recent actions whose timestamp is older than this value are returned.
Response
- Status:
200 OK - Body: object with:
has_older: boolean indicating whether more activity entries are available before the oldest entry returned.activities: array of activity entries, ordered most recent first.
Approve Purchase Order
Sample Request
import requests
import os
data = {
"reviewer_group_id": "rg_abc123",
"reviewer_id": "YfsK2SFZ8afndGo2u79ruB",
"operator_role": "Procurement Manager"
}
requests.post(
url='https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/approvals',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/approvals \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"reviewer_group_id": "rg_abc123",
"reviewer_id": "YfsK2SFZ8afndGo2u79ruB",
"operator_role": "Procurement Manager"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/approvals"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
reviewer_group_id = "rg_abc123"
reviewer_id = "YfsK2SFZ8afndGo2u79ruB"
operator_role = "Procurement Manager"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"reviewer_group_id": "rg_abc123",
"reviewer_id": "YfsK2SFZ8afndGo2u79ruB",
"operator_role": "Procurement Manager"
}
POST https://api.epsilon3.io/v1/builds/orders/:purchaseOrderId/approvals
Record an approval from a reviewer in one of the purchase order's configured reviewer groups. Used to satisfy review requirements before a purchase order can transition out of in_review.
Parameters
purchaseOrderId(path parameter): 22-character ID of the purchase order being approved.
Request Body
reviewer_group_id: (required) ID of the reviewer group the approval belongs to.reviewer_id: (required) Identifier of the user casting the approval. Can be either a user's email address or user ID.operator_role: (optional) Free-form label describing the role of the reviewer for this approval.
Response
- Status:
201 Created - Body: (empty)
List Purchase Order Shipments
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/shipments',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/shipments \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/shipments"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
GET https://api.epsilon3.io/v1/builds/orders/:purchaseOrderId/shipments
List all shipments associated with a purchase order, including each shipment's items and per-item received/rejected inventory.
Parameters
purchaseOrderId(path parameter): 22-character ID of the purchase order whose shipments are being listed.
Example Response Body
{
"shipments": [
{
"id": "5dKzL0qXk6tRb2qWmHnPtV",
"shipment_number": 1,
"status": "Delivered",
"vendor_id": "4kGbI42h7cIo0Ai3dsC9m3",
"vendor_order_id": "ACME-INV-998",
"carrier_id": "carrier_ups",
"carrier_tracking_number": "1Z999AA10123456784",
"shipment_date": "2024-06-22T00:00:00.000Z",
"estimated_delivery_date": "2024-07-01T00:00:00.000Z",
"actual_delivery_date": "2024-06-30T00:00:00.000Z",
"notes": "Delivered to receiving dock",
"attachments": [],
"items": [
{
"id": "si_abc123",
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"quantity": 1,
"purchase_order_id": "hCga2bVz70tAXnjhHbvczA",
"part": {
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"part_number": "ENGINE-1",
"revision": "C",
"name": "Rocket Engine"
},
"accepted_items": [
{
"item_id": "Uqo1eK43QThNE78Rl3HTco",
"quantity": 1,
"created_at": "2024-06-30T15:00:00.000Z",
"location_id": 12,
"unit_cost_cents": 800000,
"serial": "SN-0001"
}
],
"rejected_items": [],
"additional_rejected_quantity": 0
}
],
"created_at": "2024-06-22T10:00:00.000Z",
"updated_at": "2024-06-30T15:00:00.000Z"
}
]
}
Response
- Status:
200 OK - Body: object with
shipments, an array ofShipmentobjects (see the Shipments API section for the full shape).
List Purchase Order Received Items
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/received-items',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/received-items \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/orders/hCga2bVz70tAXnjhHbvczA/received-items"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
GET https://api.epsilon3.io/v1/builds/orders/:purchaseOrderId/received-items
List the inventory items that have been received against a purchase order. Each entry represents a single accepted-into-inventory receipt; one purchase order line may correspond to multiple entries when received in batches. Rejected receipts are not included.
Parameters
purchaseOrderId(path parameter): 22-character ID of the purchase order whose received items are being listed.
Example Response Body
[
{
"id": "roi_abc123",
"order_id": "hCga2bVz70tAXnjhHbvczA",
"shipment_id": "5dKzL0qXk6tRb2qWmHnPtV",
"item_id": "Uqo1eK43QThNE78Rl3HTco",
"location_id": "12",
"quantity": 1,
"unit_cost_cents": 800000,
"created_at": "2024-06-30T15:00:00.000Z",
"updated_at": "2024-06-30T15:00:00.000Z"
}
]
Response
- Status:
200 OK - Body: array of received-item objects. Each object contains:
id: ID of the received-item record.order_id: ID of the purchase order the receipt is associated with.shipment_id: ID of the shipment the item was received from. Omitted for older receipts that were not recorded through a shipment.item_id: ID of the inventory item created by the receipt.location_id: ID of the inventory location of the inventory item the receipt created, ornullif it has no location. For older receipts this can be free text rather than a location ID.quantity: Quantity received in this entry.unit_cost_cents: Per-unit cost, in cents, of the inventory item the receipt created. Omitted when the item has no cost. For older receipts this is the cost at the time of receipt. For newer ones it is the item's current cost, which can differ if the item was edited or merged into a lot.created_at: ISO 8601 timestamp of when the receipt was recorded.updated_at: ISO 8601 timestamp of the most recent update.
Sale Orders
List Sale Orders
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/sale-orders',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/sale-orders \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/sale-orders"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/sale-orders
Retrieve all sale orders in your workspace.
Example Response Body
{
"sale_orders": [
{
"id": "9kLmN0pQ7rStUvWxYz1a2b",
"team_id": "teamolympus",
"order_number": "SO-1",
"state": "confirmed",
"order_date": "2024-06-19T00:00:00.000Z",
"delivery_date": "2024-07-01T00:00:00.000Z",
"notes": "Priority customer",
"tracking_number": "1Z999AA10123456784",
"created_by": "YfsK2SFZ8afndGo2u79ruB",
"customer": {
"id": "d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"team_id": "teamolympus",
"name": "Globex Corporation",
"archived": false,
"discount_bp": 250,
"default_address_id": null,
"attachments": [],
"details": [],
"created_at": "2024-06-19T02:06:14.204Z",
"updated_at": "2024-06-19T02:06:14.204Z"
},
"order_lines": [
{
"id": "Lm3nOp4QrStUvWxYz12345",
"order_id": "9kLmN0pQ7rStUvWxYz1a2b",
"description": "Rocket Engine",
"unit_price_cents": 800000,
"quantity": 2,
"part": {
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"part_number": "ENGINE-1",
"revision": "C",
"name": "Rocket Engine"
},
"work_orders": [],
"assigned_inventory": [],
"fulfilled_inventory": [],
"sold_inventory": [],
"tax": {
"tax_jurisdiction": {
"id": "e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f",
"code": "CA",
"name": "California",
"tax_rate_bp": 725
},
"price_cents": 116000
},
"subtotal_price_cents": 1600000,
"total_price_cents": 1716000
}
],
"subtotal_price_cents": {
"pre_discount": 1600000,
"post_discount": 1560000
},
"tax_price_cents": {
"pre_discount": 116000,
"post_discount": 113100
},
"discount_cents": 40000,
"discount_bp": 250,
"total_price_cents": 1673100,
"details": [],
"attachments": [],
"confirmation_date": "2024-06-20T18:30:22.512Z",
"created_at": "2024-06-19T02:06:14.204Z",
"updated_at": "2024-06-20T18:30:22.512Z"
}
]
}
Response
- Status:
200 OK - Body: object with
sale_orders, an array ofSaleOrderobjects.
Monetary amounts are integers expressed in cents. Discounts are expressed in basis points (100 bp = 1%). subtotal_price_cents and tax_price_cents are each broken out into pre_discount and post_discount values; total_price_cents is the post-discount subtotal plus post-discount tax.
Get Sale Order
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
GET https://api.epsilon3.io/v1/builds/sale-orders/:saleOrderId
Retrieve a single sale order using its ID.
Parameters
saleOrderId(path parameter): UUID of the sale order being retrieved.
Example Response Body
{
"id": "9kLmN0pQ7rStUvWxYz1a2b",
"team_id": "teamolympus",
"order_number": "SO-1",
"state": "confirmed",
"order_date": "2024-06-19T00:00:00.000Z",
"delivery_date": "2024-07-01T00:00:00.000Z",
"notes": "Priority customer",
"tracking_number": "1Z999AA10123456784",
"created_by": "YfsK2SFZ8afndGo2u79ruB",
"customer": {
"id": "d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"team_id": "teamolympus",
"name": "Globex Corporation",
"archived": false,
"discount_bp": 250,
"default_address_id": null,
"attachments": [],
"details": [],
"created_at": "2024-06-19T02:06:14.204Z",
"updated_at": "2024-06-19T02:06:14.204Z"
},
"order_lines": [
{
"id": "Lm3nOp4QrStUvWxYz12345",
"order_id": "9kLmN0pQ7rStUvWxYz1a2b",
"description": "Rocket Engine",
"unit_price_cents": 800000,
"quantity": 2,
"part": {
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"part_number": "ENGINE-1",
"revision": "C",
"name": "Rocket Engine"
},
"work_orders": [
{
"id": "85",
"work_order_number": 69,
"name": "Project 1 WO 1",
"sale_order_line_id": "Lm3nOp4QrStUvWxYz12345",
"aboms": []
}
],
"assigned_inventory": [],
"fulfilled_inventory": [],
"sold_inventory": [],
"tax": {
"tax_jurisdiction": {
"id": "e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f",
"code": "CA",
"name": "California",
"tax_rate_bp": 725
},
"price_cents": 116000
},
"subtotal_price_cents": 1600000,
"total_price_cents": 1716000
}
],
"subtotal_price_cents": {
"pre_discount": 1600000,
"post_discount": 1560000
},
"tax_price_cents": {
"pre_discount": 116000,
"post_discount": 113100
},
"discount_cents": 40000,
"discount_bp": 250,
"total_price_cents": 1673100,
"details": [
{
"id": "a4ee7953-cf8e-4bbf-9313-72bfd733ef4c",
"value": {
"recorded": "SO-Q3-2024"
}
}
],
"attachments": [],
"reviewer_groups": [],
"confirmation_date": "2024-06-20T18:30:22.512Z",
"created_at": "2024-06-19T02:06:14.204Z",
"updated_at": "2024-06-20T18:30:22.512Z"
}
Response
- Status:
200 OK - Body: the full
SaleOrderobject.
Create Sale Orders
Sample Request (Single Sale Order)
import requests
import os
data = {
"sale_orders": [
{
"order_number": "SO-1",
"customer_id": "d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"state": "draft",
"order_date": "2024-06-19T00:00:00.000Z",
"delivery_date": "2024-07-01T00:00:00.000Z",
"notes": "Priority customer",
"created_by": "YfsK2SFZ8afndGo2u79ruB",
"order_lines": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"description": "Rocket Engine",
"unit_price_cents": 800000,
"quantity": 2
}
]
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/sale-orders',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/sale-orders \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"sale_orders": [
{
"order_number": "SO-1",
"customer_id": "d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"state": "draft",
"order_date": "2024-06-19T00:00:00.000Z",
"delivery_date": "2024-07-01T00:00:00.000Z",
"notes": "Priority customer",
"created_by": "YfsK2SFZ8afndGo2u79ruB",
"order_lines": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"description": "Rocket Engine",
"unit_price_cents": 800000,
"quantity": 2
}
]
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/sale-orders"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
sale_orders = @(
@{
order_number = "SO-1"
customer_id = "d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
state = "draft"
order_date = "2024-06-19T00:00:00.000Z"
delivery_date = "2024-07-01T00:00:00.000Z"
notes = "Priority customer"
created_by = "YfsK2SFZ8afndGo2u79ruB"
order_lines = @(
@{
part_revision_id = "W4e4WfDe6ZU8OZdvIaJIUw"
description = "Rocket Engine"
unit_price_cents = 800000
quantity = 2
}
)
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
POST https://api.epsilon3.io/v1/builds/sale-orders
Create one or more sale orders in your workspace. This endpoint supports bulk creation of sale orders. The system-generated id is returned in the response and cannot be supplied in the request body.
Example Request Body (Single Sale Order)
{
"sale_orders": [
{
"order_number": "SO-1",
"customer_id": "d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"state": "draft",
"order_date": "2024-06-19T00:00:00.000Z",
"delivery_date": "2024-07-01T00:00:00.000Z",
"notes": "Priority customer",
"created_by": "YfsK2SFZ8afndGo2u79ruB",
"order_lines": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"description": "Rocket Engine",
"unit_price_cents": 800000,
"quantity": 2
}
]
}
]
}
Example Request Body (Multiple Sale Orders)
{
"sale_orders": [
{
"order_number": "SO-1",
"customer_id": "d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"state": "draft",
"order_date": "2024-06-19T00:00:00.000Z",
"created_by": "YfsK2SFZ8afndGo2u79ruB",
"order_lines": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"unit_price_cents": 800000,
"quantity": 2
}
]
},
{
"order_number": "SO-2",
"customer_id": "e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f",
"state": "draft",
"order_date": "2024-06-20T00:00:00.000Z",
"created_by": "Zt3mR9pQ7wXvB2nK4jL8cD",
"discount_bp": 250,
"order_lines": [
{
"item_id": "Uqo1eK43QThNE78Rl3HTco",
"description": "Fuel Tank",
"unit_price_cents": 120000,
"quantity": 5
}
]
}
]
}
Example Response Body
{
"ids": ["9kLmN0pQ7rStUvWxYz1a2b"]
}
Request Body
sale_orders: (required) Array of sale order objects to create (1 to 1000 sale orders). Each sale order object contains:order_number: (required) Human-readable order number. Must be a non-empty string. See Get Next Sale Order Number to generate the next available value.customer_id: (required) System generated ID of the customer for this sale order.state: (required) Order state. Must be one of:draft,in_review,confirmed,closed,canceled.order_date: (required) Order date. Must be a valid ISO 8601 date.created_by: (required) The database user ID of the user creating the sale order. An email address is not accepted.delivery_date: (optional) Requested delivery date. Must be a valid ISO 8601 date.notes: (optional) Free-form notes. Must be a string of up to 4096 characters.tracking_number: (optional) Shipment tracking number. Must be a string of up to 255 characters.discount_bp: (optional) Order-level discount expressed in basis points (100 bp = 1%). Must be an integer between 1 and 10000.order_lines: (optional) Array of order line objects (0 to 1000 items). Each line contains:unit_price_cents: (required) Per-unit price in cents. Must be an integer.quantity: (required) Quantity ordered. Must be a positive number (0 is not allowed).part_revision_id: (optional) System generated ID of the specific part revision being sold.item_id: (optional) System generated ID of a specific inventory item being sold.tax_jurisdiction_id: (optional) UUID of the tax jurisdiction to apply to this line.description: (optional) Free-form description of the line item.
details: (optional) Custom sale order detail fields. Must be an array of 0 to 100 objects with:id: (required) ID of the custom field.value: (required) Object containing:recorded: (required) The value to set for the custom field.
Response
- Status:
201 Created - Body: object with
ids, an array of system-generated IDs for the created sale orders, in the same order as the request.
Get Next Sale Order Number
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/builds/sale-orders/sale-order-numbers',
params={'prefix': 'SO-'},
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X POST \
"https://api.epsilon3.io/v1/builds/sale-orders/sale-order-numbers?prefix=SO-" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/sale-orders/sale-order-numbers?prefix=SO-"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
}
POST https://api.epsilon3.io/v1/builds/sale-orders/sale-order-numbers?prefix={prefix}
Generate the next available sale order number. Useful for pre-populating order_number before calling Create Sale Orders.
Parameters
prefix(query parameter): optional string prefix for the generated order number (for example,SO-).
Example Response Body
{
"order_number": "SO-2"
}
Response
- Status:
200 OK - Body: object with
order_number, the next available sale order number.
Update Sale Order
Sample Request
import requests
import os
data = {
"state": "confirmed",
"tracking_number": "1Z999AA10123456784"
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"state": "confirmed",
"tracking_number": "1Z999AA10123456784"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
state = "confirmed"
tracking_number = "1Z999AA10123456784"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"state": "confirmed",
"tracking_number": "1Z999AA10123456784"
}
PATCH https://api.epsilon3.io/v1/builds/sale-orders/:saleOrderId
Updates values of one or more fields on a specific sale order. Line items have their own dedicated endpoint and cannot be changed here.
Parameters
saleOrderId(path parameter): UUID of the sale order being updated.
Request Body
The request body is a JSON object containing the fields to be updated. Nullable fields may be passed as null to clear their current value.
state: (optional) New order state. Must be one of:draft,in_review,confirmed,closed,canceled.customer_id: (optional) System generated ID of the customer, ornullto clear.delivery_date: (optional) Requested delivery date (ISO 8601), ornullto clear.notes: (optional) Notes (max 4096 characters), ornullto clear.tracking_number: (optional) Shipment tracking number (max 255 characters), ornullto clear.archived_at: (optional) ISO 8601 timestamp to archive the sale order, ornullto unarchive.discount_bp: (optional) Order-level discount in basis points (integer between 1 and 10000), ornullto clear.details: (optional) Custom sale order detail fields to set. Must be an array of 0 to 100 objects with:id: (required) ID of the custom field.value: (required) Object containing:recorded: (required) The value to set for the custom field.
attachments: (optional) Array of attachment objects (0 to 1000 items), ornullto clear. Each object contains:attachment_id: (required) System generated UUID of the uploaded attachment.
Response
- Status:
200 OK - Body: the full updated
SaleOrderobject (same shape asGET /v1/builds/sale-orders/:saleOrderId).
Update Sale Order Lines
Sample Request
import requests
import os
data = {
"added": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"description": "Rocket Engine",
"unit_price_cents": 800000,
"quantity": 1
}
],
"updated": [
{
"id": "Lm3nOp4QrStUvWxYz12345",
"quantity": 3
}
],
"removed": [
{
"id": "0Nc1qQ4SwkHdRzLgYzZA23"
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b/order-lines',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b/order-lines \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"added": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"description": "Rocket Engine",
"unit_price_cents": 800000,
"quantity": 1
}
],
"updated": [
{
"id": "Lm3nOp4QrStUvWxYz12345",
"quantity": 3
}
],
"removed": [
{
"id": "0Nc1qQ4SwkHdRzLgYzZA23"
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b/order-lines"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
added = @(
@{
part_revision_id = "W4e4WfDe6ZU8OZdvIaJIUw"
description = "Rocket Engine"
unit_price_cents = 800000
quantity = 1
}
)
updated = @(
@{
id = "Lm3nOp4QrStUvWxYz12345"
quantity = 3
}
)
removed = @(
@{
id = "0Nc1qQ4SwkHdRzLgYzZA23"
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"added": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"description": "Rocket Engine",
"unit_price_cents": 800000,
"quantity": 1
}
],
"updated": [
{
"id": "Lm3nOp4QrStUvWxYz12345",
"quantity": 3
}
],
"removed": [
{
"id": "0Nc1qQ4SwkHdRzLgYzZA23"
}
]
}
POST https://api.epsilon3.io/v1/builds/sale-orders/:saleOrderId/order-lines
Apply a diff of changes to a sale order's line items. This endpoint is not a replacement of all line items — only the lines referenced in added, updated, and removed are affected. Lines not mentioned are left unchanged. All three arrays are optional; if all three are empty, the endpoint is a no-op.
Parameters
saleOrderId(path parameter): UUID of the sale order whose lines are being updated.
Request Body
added: (optional) Array of new line items to append to the sale order (0 to 1000 items). Each object contains:unit_price_cents: (required) Per-unit price in cents. Must be an integer.quantity: (required) Quantity ordered. Must be a positive number (0 is not allowed).part_revision_id: (optional) System generated ID of the part revision being sold.item_id: (optional) System generated ID of an inventory item being sold.tax_jurisdiction_id: (optional) UUID of the tax jurisdiction to apply to this line.description: (optional) Free-form description of the line item.
updated: (optional) Array of existing line items to modify (0 to 1000 items). Each object contains:id: (required) ID of the existing line to update.unit_price_cents: (optional) Per-unit price in cents.quantity: (optional) Quantity ordered.part_revision_id: (optional) System generated ID of the part revision, ornullto clear.item_id: (optional) System generated ID of the inventory item, ornullto clear.tax_jurisdiction_id: (optional) UUID of the tax jurisdiction, ornullto clear.description: (optional) Free-form description, ornullto clear.
removed: (optional) Array of existing line items to delete (0 to 1000 items). Each object contains:id: (required) ID of the existing line to remove.
Response
- Status:
204 No Content - Body: (empty)
Update Inventory Assignments
Sample Request
import requests
import os
data = {
"assignments": [
{
"line_id": "Lm3nOp4QrStUvWxYz12345",
"added": [
{
"item_id": "Uqo1eK43QThNE78Rl3HTco",
"quantity": 2
}
],
"removed": [
{
"item_id": "Vrp2fL54RUioF89Sm4IUdp"
}
]
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b/inventory-assignments',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b/inventory-assignments \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"assignments": [
{
"line_id": "Lm3nOp4QrStUvWxYz12345",
"added": [
{
"item_id": "Uqo1eK43QThNE78Rl3HTco",
"quantity": 2
}
],
"removed": [
{
"item_id": "Vrp2fL54RUioF89Sm4IUdp"
}
]
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b/inventory-assignments"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
assignments = @(
@{
line_id = "Lm3nOp4QrStUvWxYz12345"
added = @(
@{
item_id = "Uqo1eK43QThNE78Rl3HTco"
quantity = 2
}
)
removed = @(
@{
item_id = "Vrp2fL54RUioF89Sm4IUdp"
}
)
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"assignments": [
{
"line_id": "Lm3nOp4QrStUvWxYz12345",
"added": [
{
"item_id": "Uqo1eK43QThNE78Rl3HTco",
"quantity": 2
}
],
"removed": [
{
"item_id": "Vrp2fL54RUioF89Sm4IUdp"
}
]
}
]
}
POST https://api.epsilon3.io/v1/builds/sale-orders/:saleOrderId/inventory-assignments
Assign or unassign inventory items to a sale order's line items. Assignments are grouped per line and applied as a diff — only the items referenced in added and removed are affected.
Parameters
saleOrderId(path parameter): UUID of the sale order whose inventory assignments are being updated.
Request Body
assignments: (required) Array of per-line assignment objects (0 to 1000 items). Each object contains:line_id: (required) ID of the sale order line the assignments apply to.added: (optional) Array of inventory items to assign to the line (0 to 1000 items). Each object contains:item_id: (required) System generated ID of the inventory item.quantity: (required) Quantity of the item to assign. Must be a positive number (0 is not allowed).
removed: (optional) Array of inventory items to unassign from the line (0 to 1000 items). Each object contains:item_id: (required) System generated ID of the inventory item.
Response
- Status:
200 OK - Body: (empty)
Update Work Order Assignments
Sample Request
import requests
import os
data = {
"added": [
{
"line_id": "Lm3nOp4QrStUvWxYz12345",
"work_order_id": "85"
}
],
"removed": [
{
"line_id": "Lm3nOp4QrStUvWxYz12345",
"work_order_id": "86"
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b/work-order-assignments',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b/work-order-assignments \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"added": [
{
"line_id": "Lm3nOp4QrStUvWxYz12345",
"work_order_id": "85"
}
],
"removed": [
{
"line_id": "Lm3nOp4QrStUvWxYz12345",
"work_order_id": "86"
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b/work-order-assignments"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
added = @(
@{
line_id = "Lm3nOp4QrStUvWxYz12345"
work_order_id = "85"
}
)
removed = @(
@{
line_id = "Lm3nOp4QrStUvWxYz12345"
work_order_id = "86"
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"added": [
{
"line_id": "Lm3nOp4QrStUvWxYz12345",
"work_order_id": "85"
}
],
"removed": [
{
"line_id": "Lm3nOp4QrStUvWxYz12345",
"work_order_id": "86"
}
]
}
POST https://api.epsilon3.io/v1/builds/sale-orders/:saleOrderId/work-order-assignments
Link or unlink work orders to a sale order's line items. Applied as a diff — only the links referenced in added and removed are affected. Both arrays are optional; if both are empty, the endpoint is a no-op.
Parameters
saleOrderId(path parameter): UUID of the sale order whose work order links are being updated.
Request Body
added: (optional) Array of work order links to create (0 to 1000 items). Each object contains:line_id: (required) ID of the sale order line to link.work_order_id: (required) System generated ID of the work order to link to the line.
removed: (optional) Array of work order links to remove (0 to 1000 items). Each object contains:line_id: (required) ID of the sale order line to unlink.work_order_id: (required) System generated ID of the work order to unlink from the line.
Response
- Status:
204 No Content - Body: (empty)
Approve Sale Order
Sample Request
import requests
import os
data = {
"reviewer_group_id": "rg_abc123",
"reviewer_id": "YfsK2SFZ8afndGo2u79ruB",
"operator_role": "Sales Manager"
}
requests.post(
url='https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b/approvals',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b/approvals \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"reviewer_group_id": "rg_abc123",
"reviewer_id": "YfsK2SFZ8afndGo2u79ruB",
"operator_role": "Sales Manager"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b/approvals"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
reviewer_group_id = "rg_abc123"
reviewer_id = "YfsK2SFZ8afndGo2u79ruB"
operator_role = "Sales Manager"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"reviewer_group_id": "rg_abc123",
"reviewer_id": "YfsK2SFZ8afndGo2u79ruB",
"operator_role": "Sales Manager"
}
POST https://api.epsilon3.io/v1/builds/sale-orders/:saleOrderId/approvals
Record an approval from a reviewer in one of the sale order's configured reviewer groups. Used to satisfy review requirements before a sale order can transition out of in_review.
Parameters
saleOrderId(path parameter): UUID of the sale order being approved.
Request Body
reviewer_group_id: (required) ID of the reviewer group the approval belongs to.reviewer_id: (required) Identifier of the user casting the approval. Can be either a user's email address or user ID.operator_role: (optional) Free-form label describing the role of the reviewer for this approval.
Response
- Status:
200 OK - Body: (empty)
List Sale Order Activity
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b/activity',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b/activity \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/sale-orders/9kLmN0pQ7rStUvWxYz1a2b/activity"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
Example Response Body
{
"has_older": false,
"activities": [
{
"id": "201",
"entity_id": "9kLmN0pQ7rStUvWxYz1a2b",
"timestamp": "2024-06-20T18:30:22.512Z",
"action": "Status Updated",
"actor_type": "user",
"actor_id": "user@epsilon3.io",
"old_value": {
"state": "draft"
},
"new_value": {
"state": "confirmed"
}
},
{
"id": "200",
"entity_id": "9kLmN0pQ7rStUvWxYz1a2b",
"timestamp": "2024-06-19T02:06:14.204Z",
"action": "Created",
"actor_type": "user",
"actor_id": "user@epsilon3.io"
}
]
}
GET https://api.epsilon3.io/v1/builds/sale-orders/:saleOrderId/activity?startBeforeTimestamp={startBeforeTimestamp}
List up to twenty five most recent actions related to the specified sale order. Use the optional startBeforeTimestamp query parameter to page through older activity.
Parameters
saleOrderId(path parameter): UUID of the sale order for which to list activity.startBeforeTimestamp(query parameter): optional ISO 8601 timestamp. When set, up to twenty five most recent actions whose timestamp is older than this value are returned.
Response
- Status:
200 OK - Body: object with:
has_older: boolean indicating whether more activity entries are available before the oldest entry returned.activities: array of activity entries, ordered most recent first.
Vendors
List Vendors
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/vendors',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/vendors \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/vendors"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/vendors
Retrieve the vendors defined in your workspace, including archived vendors. Without limit or pageToken, every matching vendor is returned in one response. Pass limit to page through the list.
Parameters
includeStatistics(query parameter): optional boolean, defaulting tofalse. Whentrue, each vendor includesavg_actual_transit_time_secs, the mean transit time in seconds across the vendor's received shipments. The field isnullwhen the vendor has no received shipments, and is omitted entirely when the parameter is nottrue.
Paging Parameters
limit(query parameter): optional maximum number of vendors to return per page. Must be an integer between1and1000. When bothlimitandpageTokenare omitted, the response contains every matching vendor and nopaginationblock.pageToken(query parameter): optional continuation token. To fetch the next page, pass back thenext_page_tokenreturned in the previous response'spaginationblock. Use the token only as returned — do not construct or parse it. A token picks up after the last vendor of the previous page, so vendors created or changed while you page do not cause other vendors to be skipped. A vendor changed while you page may be returned more than once, or, when sorting bynameor byupdated_atdescending, skipped. To be sure every existing vendor is returned, sort bycreated_at, or byupdated_atascending. Restart paging from the first page when you change the filters,sort,order, orlimit; a token used with different filters or a differentsort,order, orlimitis rejected with a 400. Whenlimitis omitted from a later page, the first page's limit is used.sort(query parameter): optional field to sort by. Must be one ofname,created_at, orupdated_at. Defaults toname. Applies with or without paging.order(query parameter): optional sort direction. Must be eitherascordesc. Defaults toasc. Results have a stable secondary sort by vendor ID.
Timestamp Parameters
Each bound is an ISO 8601 timestamp in extended format, such as 2026-09-01T00:00:00.000Z, and is inclusive. A date without a time, such as 2026-09-01, means midnight UTC at the start of that day, so createdAtBefore=2026-09-01 does not include vendors created later on September 1. To include the whole day, use createdAtBefore=2026-09-01T23:59:59.999Z. Each timestamp parameter may be specified at most once.
createdAtAfter(query parameter): optional. Return only vendors created at or after this time.createdAtBefore(query parameter): optional. Return only vendors created at or before this time.updatedAtAfter(query parameter): optional. Return only vendors changed at or after this time.updatedAtBefore(query parameter): optional. Return only vendors changed at or before this time.
A vendor's updated_at changes whenever the vendor is edited, including its emails, custom field values, and attachments, and when it is archived (deleted) or unarchived. Archived vendors stay in the list with archived: true, so updatedAtAfter also returns vendors deleted since that time. updated_at does not change when a custom field definition is changed (for example renamed or removed) or when avg_actual_transit_time_secs is recalculated. Vendors not edited since updated_at was introduced report the same updated_at as created_at.
To pick up changes since your last request, pass the newest updated_at you received minus a short overlap, such as one minute, as updatedAtAfter, and match vendors by id. A vendor's updated_at is recorded when a change starts, so a change that is still being saved during your request can have a slightly earlier time than changes you already received. The overlap returns it on your next request, along with a few vendors you have already seen.
Query parameter example usage
GET https://api.epsilon3.io/v1/builds/vendors?updatedAtAfter=2026-09-01T00:00:00.000Z&sort=updated_at&limit=100
Response fields
pagination: Paging metadata, included only when the request haslimitorpageToken.limit: The page size that was applied.total: Total number of vendors matching the filters across all pages.next_page_token: Continuation token for retrieving the next page, ornullwhen this is the last page.
created_at: ISO 8601 timestamp of when the vendor was created.updated_at: ISO 8601 timestamp of when the vendor last changed. See Timestamp Parameters above for what counts as a change.
Example Response Body
{
"vendors": [
{
"id": "4kGbI42h7cIo0Ai3dsC9m3",
"name": "Acme Engines Ltd",
"archived": false,
"notes": "Brick & Mortar are our specialties",
"address": "1428 Satellite Loop\nAnytown USA\n10101",
"contact": "310-111-1111",
"emails": [
"brian@acme.test",
"tom@acme.test"
],
"payment_terms": "Net 30",
"terms_and_conditions": "Standard terms apply",
"discount_bp": 250,
"expected_transit_time_secs": 86400,
"vendor_rating": 80,
"attachments": [
{
"attachment_id": "f48c1df3-dc39-4a1d-90aa-30205f9e8653",
"name": "agreement.pdf",
"content_type": "application/pdf"
}
],
"details": [
{
"id": "fbf9d2f0-3ba2-430e-a686-d93335edd036",
"field_id": "account_rep",
"name": "Account Rep",
"type": "Text",
"metadata": null,
"required": false,
"expression_tokens": null,
"vendor_id": "4kGbI42h7cIo0Ai3dsC9m3",
"value": {
"recorded": "Jane Doe"
}
}
],
"created_at": "2024-07-17T20:21:38.417Z",
"updated_at": "2024-07-17T20:21:38.417Z"
}
]
}
Optional fields such as notes, address, contact, payment_terms, terms_and_conditions, discount_bp, expected_transit_time_secs, and vendor_rating are omitted or null when not set.
The example above was fetched without includeStatistics. With includeStatistics=true, each vendor carries an additional "avg_actual_transit_time_secs": 91260 entry, or "avg_actual_transit_time_secs": null for a vendor with no received shipments.
The example above was also fetched without limit. With limit=1, the same vendor is returned with a pagination block next to vendors:
{
"vendors": [
...
],
"pagination": {
"limit": 1,
"total": 12,
"next_page_token": "eyJrZXkiOnsidmFsdWUiOiJBY21lIEVuZ2luZXMgTHRkIiwiaWQiOiI0a0diSTQyaDdjSW8wQWkzZHNDOW0zIn0sImRpcmVjdGlvbiI6Im5leHQiLCJsaW1pdCI6MSwic29ydEJ5IjoibmFtZSIsInNvcnRPcmRlciI6ImFzYyIsImdyb3VwQnkiOm51bGwsImZpbHRlcnMiOm51bGx9"
}
}
Retrieve Vendor
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/vendors/4kGbI42h7cIo0Ai3dsC9m3',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/vendors/4kGbI42h7cIo0Ai3dsC9m3 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/vendors/4kGbI42h7cIo0Ai3dsC9m3"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/vendors/:vendorId
Retrieve a single vendor by ID.
Parameters
vendorId(path parameter): ID of the vendor to retrieve.includeStatistics(query parameter): optional boolean. Whentrue, the vendor includes computed shipping statistics such asavg_actual_transit_time_secs.
Example Response Body
{
"id": "4kGbI42h7cIo0Ai3dsC9m3",
"name": "Acme Engines Ltd",
"archived": false,
"notes": "Brick & Mortar are our specialties",
"address": "1428 Satellite Loop\nAnytown USA\n10101",
"contact": "310-111-1111",
"emails": [
"brian@acme.test",
"tom@acme.test"
],
"attachments": [],
"details": [],
"created_at": "2024-07-17T20:21:38.417Z",
"updated_at": "2024-07-17T20:21:38.417Z"
}
Response
- Status:
200 OK, or404 Not Foundif no vendor exists with the given ID.
Create Vendors
Sample Request
import requests
import os
data = {
"vendors": [
{
"name": "Acme Engines Ltd",
"address": "1428 Satellite Loop\nAnytown USA\n10101",
"contact": "310-111-1111",
"emails": ["brian@acme.test"],
"payment_terms": "Net 30",
"discount_bp": 250
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/vendors',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/vendors \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"vendors": [
{
"name": "Acme Engines Ltd",
"contact": "310-111-1111",
"emails": ["brian@acme.test"],
"payment_terms": "Net 30",
"discount_bp": 250
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/vendors"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
vendors = @(
@{
name = "Acme Engines Ltd"
contact = "310-111-1111"
emails = @("brian@acme.test")
payment_terms = "Net 30"
discount_bp = 250
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
POST https://api.epsilon3.io/v1/builds/vendors
Create one or more vendors in your workspace. This endpoint supports bulk creation of up to 1000 vendors. Vendor names must be unique within the workspace, and the comparison is case-sensitive. A name that collides with another vendor in the same request, or with a vendor that already exists in the workspace, fails the whole request with 409 Conflict.
Example Response Body
{
"ids": ["4kGbI42h7cIo0Ai3dsC9m3"]
}
Request Body
vendors: (required) Array of vendor objects to create (1 to 1000 vendors). Each vendor object contains:name: (required) Vendor name. Must be a non-empty string of up to 255 characters, and unique (case-sensitive) among the vendors in the workspace, including archived ones.address: (optional) Vendor address. Must be a string of up to 1000 characters.contact: (optional) Contact information such as a phone number. Must be a string of up to 255 characters.notes: (optional) Free-form notes. Must be a string of up to 1000 characters.emails: (optional) Array of up to 100 email addresses. Each must be a valid email, and emails must be unique per vendor.archived: (optional) Whether the vendor is archived. Must be a boolean. Defaults tofalse.payment_terms: (optional) Payment terms. Must be a string of up to 1000 characters.terms_and_conditions: (optional) Terms and conditions. Must be a string of up to 1000 characters.discount_bp: (optional) Default discount expressed in basis points (100 bp = 1%). Must be an integer between 1 and 10000.expected_transit_time_secs: (optional) Expected shipping transit time in seconds. Must be a positive integer.vendor_rating: (optional) Vendor rating. Must be an integer between 1 and 100.details: (optional) Custom detail fields (0 to 500 objects). Each contains anid(custom field ID) and avalueobject withrecorded.
Files are attached to a vendor after it is created, with the Attach File to Vendor endpoint.
Response
- Status:
201 Created, or409 Conflictif a vendor name is already taken or an email address is repeated on the same vendor. - Body: object with
ids, an array of system-generated IDs for the created vendors, in the same order as the request.
Update Vendor
Sample Request
import requests
import os
data = {
"payment_terms": "Net 45"
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/vendors/4kGbI42h7cIo0Ai3dsC9m3',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/vendors/4kGbI42h7cIo0Ai3dsC9m3 \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"payment_terms": "Net 45"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/vendors/4kGbI42h7cIo0Ai3dsC9m3"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
payment_terms = "Net 45"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
PATCH https://api.epsilon3.io/v1/builds/vendors/:vendorId
Update an existing vendor. Only the fields included in the request body are changed. An archived vendor must be unarchived (by setting archived to false) before other fields can be changed.
Parameters
vendorId(path parameter): ID of the vendor being updated.
Example Request Body
{
"payment_terms": "Net 45"
}
Request Body
name: (optional) Vendor name. Must be a non-empty string of up to 255 characters, and unique (case-sensitive) among the vendors in the workspace, including archived ones.address: (optional) Vendor address. Must be a string of up to 1000 characters, ornullto clear it.contact: (optional) Contact information. Must be a string of up to 255 characters, ornullto clear it.notes: (optional) Free-form notes. Must be a string of up to 1000 characters, ornullto clear it.emails: (optional) Array of up to 100 email addresses. Replaces the vendor's existing emails; pass an empty array to clear them.archived: (optional) Whether the vendor is archived. Must be a boolean.payment_terms: (optional) Payment terms. Must be a string of up to 1000 characters, ornullto clear it.terms_and_conditions: (optional) Terms and conditions. Must be a string of up to 1000 characters, ornullto clear it.discount_bp: (optional) Default discount in basis points (100 bp = 1%). Must be an integer between 1 and 10000, ornullto clear it.expected_transit_time_secs: (optional) Expected shipping transit time in seconds. Must be a positive integer, ornullto clear it.vendor_rating: (optional) Vendor rating. Must be an integer between 1 and 100, ornullto clear it.details: (optional) Custom detail fields, each with anidand avalueobject withrecorded.
Attachments are not part of this request body. Use the Attach File to Vendor and Delete Vendor Attachments endpoints instead.
Response
- Status:
200 OK,404 Not Foundif no vendor exists with the given ID, or409 Conflictif the new name is already taken by another vendor or an email address is repeated. - Body: the updated vendor object, in the same shape as the Retrieve Vendor response.
Delete Vendors
Sample Request
import requests
import os
data = {
"ids": ["4kGbI42h7cIo0Ai3dsC9m3", "vnWtKWleNWiuSQbBPNdE5F"]
}
requests.delete(
url='https://api.epsilon3.io/v1/builds/vendors',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X DELETE \
https://api.epsilon3.io/v1/builds/vendors \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"ids": ["4kGbI42h7cIo0Ai3dsC9m3", "vnWtKWleNWiuSQbBPNdE5F"]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/vendors"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
ids = @("4kGbI42h7cIo0Ai3dsC9m3", "vnWtKWleNWiuSQbBPNdE5F")
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
DELETE https://api.epsilon3.io/v1/builds/vendors
Archive one or more vendors, up to 1000 per request. Vendors are never hard-deleted because existing purchase orders, shipments, and quotes may reference them; this endpoint is equivalent to updating each vendor with archived set to true. Archived vendors can be restored with the Update Vendor endpoint.
Archiving a vendor that is already archived succeeds and reports the vendor in archived_vendor_ids.
Example Request Body
{
"ids": ["4kGbI42h7cIo0Ai3dsC9m3", "vnWtKWleNWiuSQbBPNdE5F"]
}
Example Response Body
{
"archived_vendor_ids": ["4kGbI42h7cIo0Ai3dsC9m3"],
"invalid_vendor_ids": ["vnWtKWleNWiuSQbBPNdE5F"]
}
Request Body
ids: (required) Array of 1 to 1000 vendor IDs to archive. Each must be a non-empty string of up to 255 characters.
Response
- Status:
200 OKwhen at least one vendor is archived;404 Not Foundwhen none of the requested IDs match a vendor. - Body: object with
archived_vendor_ids(the IDs that were archived) and, when some IDs did not match,invalid_vendor_ids(the IDs that matched no vendor).
Attach File to Vendor
Sample Request
import requests
import os
with open('quote.pdf', 'rb') as file:
requests.post(
url='https://api.epsilon3.io/v1/builds/vendors/4kGbI42h7cIo0Ai3dsC9m3/attachments',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
data={'name': 'quote.pdf'},
files={'file': file}
)
curl -X POST \
https://api.epsilon3.io/v1/builds/vendors/4kGbI42h7cIo0Ai3dsC9m3/attachments \
-u "$EPSILON3_API_KEY": \
-F "name=quote.pdf" \
-F "file=@quote.pdf"
$apiUrl = "https://api.epsilon3.io/v1/builds/vendors/4kGbI42h7cIo0Ai3dsC9m3/attachments"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$form = @{
name = "quote.pdf"
file = Get-Item -Path "quote.pdf"
}
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
} -Form $form
POST https://api.epsilon3.io/v1/builds/vendors/:vendorId/attachments
Uploads the provided file and attaches it to the specified vendor. Attached files are returned in the attachments array of the vendor object.
Parameters
vendorId(path parameter): ID of the vendor the file is attached to.
Example Response Body
{
"attachment_id": "f48c1df3-dc39-4a1d-90aa-30205f9e8653",
"name": "quote.pdf",
"content_type": "application/pdf"
}
Request Body
This request should be sent as multipart/form-data containing the following fields:
name: (required) Name of the file. Must be a string of up to 255 characters.file: (required) The file being attached. File size must not exceed500 MB.
Response
- Status:
201 Created - Body: object with the stored attachment:
attachment_id: System-generated ID of the stored filename: Name of the filecontent_type: MIME type of the file
Delete Vendor Attachments
Sample Request
import requests
import os
data = {
"ids": ["f48c1df3-dc39-4a1d-90aa-30205f9e8653"]
}
requests.delete(
url='https://api.epsilon3.io/v1/builds/vendors/4kGbI42h7cIo0Ai3dsC9m3/attachments',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X DELETE \
https://api.epsilon3.io/v1/builds/vendors/4kGbI42h7cIo0Ai3dsC9m3/attachments \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"ids": ["f48c1df3-dc39-4a1d-90aa-30205f9e8653"]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/vendors/4kGbI42h7cIo0Ai3dsC9m3/attachments"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
ids = @("f48c1df3-dc39-4a1d-90aa-30205f9e8653")
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
DELETE https://api.epsilon3.io/v1/builds/vendors/:vendorId/attachments
Remove one or more attachments from the specified vendor and delete the stored files. IDs that are not attached to this vendor are returned in invalid_ids.
Parameters
vendorId(path parameter): ID of the vendor the attachments belong to.
Example Request Body
{
"ids": ["f48c1df3-dc39-4a1d-90aa-30205f9e8653"]
}
Example Response Body
{
"deleted_ids": ["f48c1df3-dc39-4a1d-90aa-30205f9e8653"]
}
Request Body
ids: (required) Array of 1 to 1000 attachment IDs to remove from the vendor. Each must be a UUID, as returned inattachment_idby the Attach File to Vendor and Retrieve Vendor endpoints.
Response
- Status:
200 OKwhen at least one attachment is removed;404 Not Foundwhen none of the requested IDs are attached to the vendor. - Body: object with
deleted_ids(the IDs that were removed) and, when some IDs did not match,invalid_ids(the IDs that were not attached to the vendor).
Product Catalog
List Vendor Products
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/product-catalog',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/product-catalog \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/product-catalog"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/product-catalog
Retrieve every vendor product in your product catalog. Each entry maps a vendor's purchasable product to an internal part.
Example Response Body
{
"products": [
{
"id": "8f2b6c1a-3d4e-4f5a-9b8c-7d6e5f4a3b2c",
"vendor": {
"id": "4kGbI42h7cIo0Ai3dsC9m3",
"name": "Acme Engines Ltd",
"address": "1428 Satellite Loop\nAnytown USA\n10101",
"contact": "310-111-1111",
"notes": "Brick & Mortar are our specialties",
"archived": false,
"payment_terms": "Net 30",
"terms_and_conditions": null,
"discount_bp": null,
"expected_transit_time_secs": null,
"vendor_rating": null,
"emails": ["brian@acme.test"],
"created_at": "2024-07-17T20:21:38.417Z",
"updated_at": "2024-07-17T20:21:38.417Z",
"attachments": []
},
"name": "Rocket Engine (OEM)",
"product_id": "ACME-ENG-1",
"archived": false,
"unit_cost_cents": 800000,
"units": "each",
"minimum_order_quantity": 1,
"lead_time_in_days": 30,
"notes": "Bulk discounts available over 10 units",
"internal_mappings": [
{
"mapping_type": "Part",
"is_preferred": true,
"part": {
"id": "66qVqW9X5NKz7qPJhrlFtG",
"part_no": "ENGINE-1",
"name": "Rocket Engine",
"rev": "C",
...
}
}
]
}
]
}
The vendor object has the same shape as the vendors returned by the List Vendors endpoint above. The part object inside each internal_mappings entry is the full part object, the same shape returned by the Get Part endpoint (GET https://api.epsilon3.io/v1/builds/parts/:partId).
Upsert Vendor Products
Sample Request
import requests
import os
data = {
"products": [
{
"vendor_id": "4kGbI42h7cIo0Ai3dsC9m3",
"name": "Rocket Engine (OEM)",
"product_id": "ACME-ENG-1",
"archived": False,
"unit_cost_cents": 800000,
"units": "each",
"minimum_order_quantity": 1,
"lead_time_in_days": 30,
"notes": "Bulk discounts available over 10 units",
"internal_mappings": [
{
"mapping_type": "Part",
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"is_preferred": True
}
]
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/product-catalog',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/product-catalog \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"products": [
{
"vendor_id": "4kGbI42h7cIo0Ai3dsC9m3",
"name": "Rocket Engine (OEM)",
"product_id": "ACME-ENG-1",
"archived": false,
"unit_cost_cents": 800000,
"units": "each",
"minimum_order_quantity": 1,
"lead_time_in_days": 30,
"notes": "Bulk discounts available over 10 units",
"internal_mappings": [
{
"mapping_type": "Part",
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"is_preferred": true
}
]
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/product-catalog"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
products = @(
@{
vendor_id = "4kGbI42h7cIo0Ai3dsC9m3"
name = "Rocket Engine (OEM)"
product_id = "ACME-ENG-1"
archived = $false
unit_cost_cents = 800000
units = "each"
minimum_order_quantity = 1
lead_time_in_days = 30
notes = "Bulk discounts available over 10 units"
internal_mappings = @(
@{
mapping_type = "Part"
part_id = "66qVqW9X5NKz7qPJhrlFtG"
is_preferred = $true
}
)
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
POST https://api.epsilon3.io/v1/builds/product-catalog
Create or update one or more vendor products (1 to 1000 per request). Include a product's id to update an existing catalog entry; omit it to create a new one. A single vendor_id may not have two products that share the same product_id.
Example Request Body
{
"products": [
{
"vendor_id": "4kGbI42h7cIo0Ai3dsC9m3",
"name": "Rocket Engine (OEM)",
"product_id": "ACME-ENG-1",
"archived": false,
"unit_cost_cents": 800000,
"units": "each",
"minimum_order_quantity": 1,
"lead_time_in_days": 30,
"notes": "Bulk discounts available over 10 units",
"internal_mappings": [
{
"mapping_type": "Part",
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"is_preferred": true
}
]
}
]
}
Request Body
products: (required) Array of vendor product objects to create or update (1 to 1000 products). Each product object contains:id: (optional) System generated ID of an existing catalog entry. Provide it to update that entry; omit it to create a new one.vendor_id: (required) System generated ID of the vendor that supplies this product.name: (required) Display name of the vendor product. Must be a string of up to 255 characters.product_id: (required) The vendor's own identifier for the product (e.g. a catalog or SKU number). Must be a string of up to 255 characters and unique per vendor.archived: (required) Whether the catalog entry is archived. Must be a boolean.unit_cost_cents: (required) Per-unit cost in cents. Must be a non-negative integer.units: (optional) Unit of measure for the product (e.g.each). Must be a string of up to 255 characters.minimum_order_quantity: (optional) Smallest quantity the vendor will accept on an order. Must be a positive number.lead_time_in_days: (optional) Expected lead time. Must be an integer between 1 and 3650.notes: (optional) Free-form notes. Must be a string of up to 1000 characters.internal_mappings: (required) Array containing exactly one mapping to an internal part. The mapping object contains:mapping_type: (required) Must bePart.part_id: (optional) System generated ID of the internal part this product maps to. Provide exactly one ofpart_idorpart_no.part_no: (optional) Part number of the internal part this product maps to, matched case-insensitively. Provide exactly one ofpart_idorpart_no. The part number must resolve to a part you have access to, otherwise the request is rejected.is_preferred: (optional) Whether this vendor product is the preferred source for the mapped part. Must be a boolean.
Response
- Status:
200 OK - Body: empty.
Purchase Requests
List Purchase Requests
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/purchase-requests',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/purchase-requests \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/purchase-requests"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/purchase-requests
Retrieve all purchase requests in your workspace.
Example Response Body
{
"requests": [
{
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"team_id": "teamolympus",
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"part": {
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"project_id": null,
"part_number": "ENGINE-1",
"revision": "C",
"name": "Rocket Engine"
},
"quantity": 5,
"requesting_user_id": "YfsK2SFZ8afndGo2u79ruB",
"needed_by_date": "09/01/2024",
"disposition": "Pending",
"disposition_user_id": null,
"linked_order_id": null,
"notes": null,
"created_at": "2024-06-19T02:06:14.204Z",
"updated_at": "2024-06-19T02:06:14.204Z"
}
]
}
Each request's disposition is one of Pending, Approved, or Rejected. The part object is omitted for parts the caller does not have permission to view.
Create Purchase Requests
Sample Request
import requests
import os
data = {
"requests": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"quantity": 5,
"needed_by_date": "09/01/2024"
}
],
"created_by": "jane.doe@example.com"
}
requests.post(
url='https://api.epsilon3.io/v1/builds/purchase-requests',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/purchase-requests \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"requests": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"quantity": 5,
"needed_by_date": "09/01/2024"
}
],
"created_by": "jane.doe@example.com"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/purchase-requests"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
requests = @(
@{
part_revision_id = "W4e4WfDe6ZU8OZdvIaJIUw"
quantity = 5
needed_by_date = "09/01/2024"
}
)
created_by = "jane.doe@example.com"
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
POST https://api.epsilon3.io/v1/builds/purchase-requests
Create one or more purchase requests in your workspace. This endpoint supports bulk creation of up to 100 requests. New requests start with a Pending disposition.
Example Request Body
{
"requests": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"quantity": 5,
"needed_by_date": "09/01/2024"
}
],
"created_by": "jane.doe@example.com"
}
Example Response Body
{
"ids": ["1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"]
}
Request Body
requests: (required) Array of purchase request objects to create (1 to 100 requests). Each request object contains:part_revision_id: (required) System generated ID of the part revision being requested. Must be a string of up to 255 characters.quantity: (required) Quantity requested. Must be an integer of at least 1.needed_by_date: (required) Date the parts are needed by, inMM/DD/YYYYformat (e.g.09/01/2024). Must be a string of up to 255 characters.
created_by: (required for Team API keys) Identifier of the user creating the requests. Can be either a user's email address or user ID. Not required for User API keys, which attribute the requests to the key's user.
Response
- Status:
201 Created - Body: object with
ids, an array of system-generated UUIDs for the created requests, in the same order as the request.
Disposition Purchase Request
Sample Request
import requests
import os
data = {
"disposition": "Approved",
"linked_order_id": "hCga2bVz70tAXnjhHbvczA",
"notes": "Approved and added to PO-1",
"disposition_user": "jane.doe@example.com"
}
requests.put(
url='https://api.epsilon3.io/v1/builds/purchase-requests/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d/disposition',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PUT \
https://api.epsilon3.io/v1/builds/purchase-requests/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d/disposition \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"disposition": "Approved",
"linked_order_id": "hCga2bVz70tAXnjhHbvczA",
"notes": "Approved and added to PO-1",
"disposition_user": "jane.doe@example.com"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/purchase-requests/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d/disposition"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
disposition = "Approved"
linked_order_id = "hCga2bVz70tAXnjhHbvczA"
notes = "Approved and added to PO-1"
disposition_user = "jane.doe@example.com"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Put -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
PUT https://api.epsilon3.io/v1/builds/purchase-requests/:requestId/disposition
Approve or reject a purchase request. Optionally link it to the purchase order created to fulfill it.
Parameters
requestId(path parameter): UUID of the purchase request being dispositioned.
Example Request Body
{
"disposition": "Approved",
"linked_order_id": "hCga2bVz70tAXnjhHbvczA",
"notes": "Approved and added to PO-1",
"disposition_user": "jane.doe@example.com"
}
Request Body
disposition: (required) New disposition for the request. Must be eitherApprovedorRejected.linked_order_id: (optional) System generated ID of the purchase order this request was fulfilled by. Must be a string of up to 255 characters.notes: (optional) Free-form notes about the disposition. Must be a string of up to 255 characters.disposition_user: (required for Team API keys) Identifier of the user performing the disposition. Can be either a user's email address or user ID. Not required for User API keys, which attribute the disposition to the key's user.
Response
- Status:
200 OK - Body: the full updated purchase request object (same shape as an entry in the List Purchase Requests response).
Vendor Quotes
List Vendor Quotes
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/vendor-quotes',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/vendor-quotes \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/vendor-quotes"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/vendor-quotes
Retrieve all vendor quotes in your workspace.
Example Response Body
{
"quotes": [
{
"id": "9f8e7d6c-5b4a-4321-9876-543210fedcba",
"team_id": "teamolympus",
"code": 1,
"display_code": "QT-1",
"vendor_id": "4kGbI42h7cIo0Ai3dsC9m3",
"disposition": "Pending",
"line_items": [
{
"id": "0a1b2c3d-4e5f-4061-8273-849506172839",
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"part": {
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"project_id": null,
"part_number": "ENGINE-1",
"revision": "C",
"name": "Rocket Engine"
},
"quantity": 2,
"unit_cost_cents": 800000,
"created_at": "2024-06-19T02:06:14.204Z"
}
],
"sales_tax_basis_points": 825,
"tax_jurisdiction_id": "a4ee7953-cf8e-4bbf-9313-72bfd733ef4c",
"attachments": [],
"creator_user_id": "YfsK2SFZ8afndGo2u79ruB",
"disposition_user_id": null,
"linked_order_id": null,
"notes": null,
"created_at": "2024-06-19T02:06:14.204Z",
"updated_at": "2024-06-19T02:06:14.204Z"
}
]
}
Each quote's disposition is one of Pending, Approved, or Rejected. A line item's part object is omitted for parts the caller does not have permission to view.
Create Vendor Quote
Sample Request
import requests
import os
data = {
"vendor_id": "4kGbI42h7cIo0Ai3dsC9m3",
"line_items": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"quantity": 2,
"unit_cost_cents": 800000
}
],
"sales_tax_basis_points": 825,
"tax_jurisdiction_id": "a4ee7953-cf8e-4bbf-9313-72bfd733ef4c",
"created_by": "jane.doe@example.com"
}
requests.post(
url='https://api.epsilon3.io/v1/builds/vendor-quotes',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/vendor-quotes \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"vendor_id": "4kGbI42h7cIo0Ai3dsC9m3",
"line_items": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"quantity": 2,
"unit_cost_cents": 800000
}
],
"sales_tax_basis_points": 825,
"tax_jurisdiction_id": "a4ee7953-cf8e-4bbf-9313-72bfd733ef4c",
"created_by": "jane.doe@example.com"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/vendor-quotes"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
vendor_id = "4kGbI42h7cIo0Ai3dsC9m3"
line_items = @(
@{
part_revision_id = "W4e4WfDe6ZU8OZdvIaJIUw"
quantity = 2
unit_cost_cents = 800000
}
)
sales_tax_basis_points = 825
tax_jurisdiction_id = "a4ee7953-cf8e-4bbf-9313-72bfd733ef4c"
created_by = "jane.doe@example.com"
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
POST https://api.epsilon3.io/v1/builds/vendor-quotes
Create a vendor quote in your workspace. New quotes start with a Pending disposition. The system-generated code and display_code (e.g. QT-1) are assigned automatically.
Example Request Body
{
"vendor_id": "4kGbI42h7cIo0Ai3dsC9m3",
"line_items": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"quantity": 2,
"unit_cost_cents": 800000
}
],
"sales_tax_basis_points": 825,
"tax_jurisdiction_id": "a4ee7953-cf8e-4bbf-9313-72bfd733ef4c",
"created_by": "jane.doe@example.com"
}
Example Response Body
{
"id": "9f8e7d6c-5b4a-4321-9876-543210fedcba"
}
Request Body
vendor_id: (required) System generated ID of the vendor providing the quote. Must be a string of up to 255 characters.line_items: (required) Array of quoted line items (0 to 1000 items). Each line item contains:part_revision_id: (required) System generated ID of the quoted part revision. Must be a string of up to 255 characters.quantity: (required) Quantity quoted. Must be an integer of at least 1.unit_cost_cents: (required) Per-unit cost in cents. Must be a non-negative integer.
attachments: (optional) Array of attachment objects. Each object contains anattachment_idof an uploaded attachment.sales_tax_basis_points: (optional) Sales tax expressed in basis points (100 bp = 1%). Must be an integer between 0 and 10000.tax_jurisdiction_id: (optional) ID of the tax jurisdiction to apply to the quote. Must be a string of up to 255 characters.created_by: (required for Team API keys) Identifier of the user creating the quote. Can be either a user's email address or user ID. Not required for User API keys, which attribute the quote to the key's user.
Response
- Status:
201 Created - Body: object with
id, the system-generated UUID of the created quote.
Attach Files to Vendor Quote
Sample Request
import requests
import os
with open('traveler.pdf', 'rb') as traveler, open('torque.png', 'rb') as torque:
requests.post(
url='https://api.epsilon3.io/v1/builds/vendor-quotes/3b6f0a52-8a4e-4d7f-9d61-2f1c8e0b7a44/files',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
files=[('file', traveler), ('file', torque)]
)
curl -X POST \
https://api.epsilon3.io/v1/builds/vendor-quotes/3b6f0a52-8a4e-4d7f-9d61-2f1c8e0b7a44/files \
-u "$EPSILON3_API_KEY": \
-F "file=@traveler.pdf" \
-F "file=@torque.png"
$apiUrl = "https://api.epsilon3.io/v1/builds/vendor-quotes/3b6f0a52-8a4e-4d7f-9d61-2f1c8e0b7a44/files"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$form = @{
file = @(Get-Item -Path "traveler.pdf"; Get-Item -Path "torque.png")
}
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
} -Form $form
POST https://api.epsilon3.io/v1/builds/vendor-quotes/:quoteId/files
Uploads up to 10 files and adds them to the attachments of the specified vendor quote. Existing attachments are kept. Either every file is attached or none is. Attached files are returned in the attachments array of the vendor quote.
Parameters
quoteId(path parameter): ID of the vendor quote the files are attached to.
Example Response Body
{
"attachments": [
{
"attachment_id": "f48c1df3-dc39-4a1d-90aa-30205f9e8653",
"name": "traveler.pdf",
"content_type": "application/pdf"
},
{
"attachment_id": "0b8d6c1e-6a0f-4d43-9f53-4d1e8a2f7c11",
"name": "torque.png",
"content_type": "image/png"
}
]
}
Request Body
This request should be sent as multipart/form-data containing the following fields:
file: (required) A file to attach. Repeat the field to attach up to 10 files in one request. Each file must not exceed500 MB.attachment_id: (optional) A UUID to store the file under. Send it once per file, in the same order as the files. When omitted, an ID is generated.name: (optional) The attachment name, up to 255 characters. Send it once per file, in the same order as the files. When omitted, the name of the uploaded file is used.
The content type, size and source of each attachment are set from the uploaded file. Sending attachment_id or name a different number of times than file, or an attachment_id that is already in use, returns 422 Unprocessable Entity.
Response
- Status:
201 Created - Body: object with the stored attachments, in upload order:
attachments: Array of attachments, each with:attachment_id: ID of the stored filename: Name of the attachmentcontent_type: MIME type of the file
Disposition Vendor Quote
Sample Request
import requests
import os
data = {
"disposition": "Approved",
"linked_order_id": "hCga2bVz70tAXnjhHbvczA",
"notes": "Approved; converted to PO-1",
"disposition_user": "jane.doe@example.com"
}
requests.put(
url='https://api.epsilon3.io/v1/builds/vendor-quotes/9f8e7d6c-5b4a-4321-9876-543210fedcba/disposition',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PUT \
https://api.epsilon3.io/v1/builds/vendor-quotes/9f8e7d6c-5b4a-4321-9876-543210fedcba/disposition \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"disposition": "Approved",
"linked_order_id": "hCga2bVz70tAXnjhHbvczA",
"notes": "Approved; converted to PO-1",
"disposition_user": "jane.doe@example.com"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/vendor-quotes/9f8e7d6c-5b4a-4321-9876-543210fedcba/disposition"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
disposition = "Approved"
linked_order_id = "hCga2bVz70tAXnjhHbvczA"
notes = "Approved; converted to PO-1"
disposition_user = "jane.doe@example.com"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Put -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
PUT https://api.epsilon3.io/v1/builds/vendor-quotes/:quoteId/disposition
Approve or reject a vendor quote. Optionally link it to the purchase order it was converted into.
Parameters
quoteId(path parameter): UUID of the vendor quote being dispositioned.
Example Request Body
{
"disposition": "Approved",
"linked_order_id": "hCga2bVz70tAXnjhHbvczA",
"notes": "Approved; converted to PO-1",
"disposition_user": "jane.doe@example.com"
}
Request Body
disposition: (required) New disposition for the quote. Must be eitherApprovedorRejected.linked_order_id: (optional) System generated ID of the purchase order this quote was converted into. Must be a string of up to 255 characters.notes: (optional) Free-form notes about the disposition. Must be a string of up to 255 characters.disposition_user: (required for Team API keys) Identifier of the user performing the disposition. Can be either a user's email address or user ID. Not required for User API keys, which attribute the disposition to the key's user.
Response
- Status:
200 OK - Body: the full updated vendor quote object (same shape as an entry in the List Vendor Quotes response).
Carriers
List Carriers
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/carriers',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/carriers \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/carriers"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/carriers
Retrieve all shipping carriers defined in your workspace.
Example Response Body
{
"carriers": [
{
"id": "c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"archived": false,
"team_id": "teamolympus",
"name": "USPS",
"website": "http://usps.com",
"description": "Domestic ground and priority mail",
"created_at": "2024-06-19T02:06:14.204Z",
"updated_at": "2024-06-19T02:06:14.204Z"
}
]
}
website and description are empty strings when not set.
Get Carrier
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/carriers/c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/carriers/c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/carriers/c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
GET https://api.epsilon3.io/v1/builds/carriers/:carrierId
Retrieve a single shipping carrier using its ID.
Parameters
carrierId(path parameter): ID of the carrier being retrieved.
Example Response Body
{
"id": "c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"archived": false,
"team_id": "teamolympus",
"name": "USPS",
"website": "http://usps.com",
"description": "Domestic ground and priority mail",
"created_at": "2024-06-19T02:06:14.204Z",
"updated_at": "2024-06-19T02:06:14.204Z"
}
Create Carriers
Sample Request
import requests
import os
data = {
"carriers": [
{
"name": "FedEx",
"description": "Express and freight shipping",
"website": "fedex.com"
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/carriers',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/carriers \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"carriers": [
{
"name": "FedEx",
"description": "Express and freight shipping",
"website": "fedex.com"
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/carriers"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
carriers = @(
@{
name = "FedEx"
description = "Express and freight shipping"
website = "fedex.com"
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
POST https://api.epsilon3.io/v1/builds/carriers
Create one or more shipping carriers in your workspace. New carriers are not archived; archive an existing carrier with the update endpoint. Carrier names are unique within a workspace: creating a carrier whose name already exists updates that carrier instead of creating a duplicate, and a single request cannot repeat the same name.
Example Request Body
{
"carriers": [
{
"name": "FedEx",
"description": "Express and freight shipping",
"website": "fedex.com"
}
]
}
Example Response Body
{
"ids": ["c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"]
}
Request Body
carriers: (required) Array of carrier objects to create. Each carrier object contains:name: (required) Carrier name. Must be a non-empty string of up to 50 characters.description: (optional) Free-form description. Must be a string of up to 512 characters.website: (optional) Carrier website. Must be a valid domain (e.g.fedex.comorhttps://fedex.com) ornull.
Response
- Status:
201 Created - Body: object with
ids, an array of system-generated IDs for the created carriers, in the same order as the request.
Update Carrier
Sample Request
import requests
import os
data = {
"description": "Express, freight, and international"
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/carriers/c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/carriers/c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"description": "Express, freight, and international"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/carriers/c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
description = "Express, freight, and international"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
PATCH https://api.epsilon3.io/v1/builds/carriers/:carrierId
Update an existing shipping carrier. Only the fields included in the request body are changed.
Parameters
carrierId(path parameter): ID of the carrier being updated.
Example Request Body
{
"description": "Express, freight, and international"
}
Request Body
name: (optional) Carrier name. Must be a non-empty string of up to 50 characters.description: (optional) Free-form description. Must be a string of up to 512 characters.website: (optional) Carrier website. Must be a valid domain (e.g.fedex.comorhttps://fedex.com) ornull.archived: (optional) Whether the carrier is archived. Must be a boolean.
Response
- Status:
204 No Content - Body: empty.
Customers
List Customers
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/customers',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/customers \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/customers"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/customers
Retrieve all customers defined in your workspace.
Example Response Body
{
"customers": [
{
"id": "d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"team_id": "teamolympus",
"name": "Globex Corporation",
"archived": false,
"notes": "Net 30 preferred",
"payment_terms": "Net 30",
"terms_and_conditions": "Standard terms apply",
"discount_bp": 250,
"default_address_id": null,
"attachments": [
{
"attachment_id": "f48c1df3-dc39-4a1d-90aa-30205f9e8653",
"name": "agreement.pdf",
"content_type": "application/pdf"
}
],
"details": [
{
"id": "fbf9d2f0-3ba2-430e-a686-d93335edd036",
"field_id": "account_rep",
"name": "Account Rep",
"type": "Text",
"metadata": null,
"required": false,
"expression_tokens": null,
"customer_id": "d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"value": {
"recorded": "Jane Doe"
}
}
],
"created_at": "2024-06-19T02:06:14.204Z",
"updated_at": "2024-06-19T02:06:14.204Z"
}
]
}
notes, payment_terms, terms_and_conditions, and discount_bp are omitted when not set.
Create Customers
Sample Request
import requests
import os
data = {
"customers": [
{
"name": "Globex Corporation",
"archived": False,
"notes": "Net 30 preferred",
"payment_terms": "Net 30",
"discount_bp": 250
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/customers',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/customers \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"customers": [
{
"name": "Globex Corporation",
"archived": false,
"notes": "Net 30 preferred",
"payment_terms": "Net 30",
"discount_bp": 250
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/customers"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
customers = @(
@{
name = "Globex Corporation"
archived = $false
notes = "Net 30 preferred"
payment_terms = "Net 30"
discount_bp = 250
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
POST https://api.epsilon3.io/v1/builds/customers
Create one or more customers in your workspace. This endpoint supports bulk creation of up to 1000 customers. Customer names must be unique within the request.
Example Request Body
{
"customers": [
{
"name": "Globex Corporation",
"archived": false,
"notes": "Net 30 preferred",
"payment_terms": "Net 30",
"discount_bp": 250
}
]
}
Example Response Body
{
"ids": ["d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"]
}
Request Body
customers: (required) Array of customer objects to create (1 to 1000 customers). Each customer object contains:name: (required) Customer name. Must be a non-empty string of up to 255 characters and unique within the request.archived: (required) Whether the customer is archived. Must be a boolean.notes: (optional) Free-form notes. Must be a string of up to 1000 characters.payment_terms: (optional) Payment terms. Must be a string of up to 1000 characters.terms_and_conditions: (optional) Terms and conditions. Must be a string of up to 1000 characters.discount_bp: (optional) Default discount expressed in basis points (100 bp = 1%). Must be an integer between 1 and 10000.attachments: (optional) Array of attachment objects (0 to 1000 items). Each object contains anattachment_idof an uploaded attachment.details: (optional) Custom detail fields (0 to 500 objects). Each contains anid(custom field ID) and avalueobject withrecorded.
Response
- Status:
201 Created - Body: object with
ids, an array of system-generated IDs for the created customers, in the same order as the request.
Update Customer
Sample Request
import requests
import os
data = {
"payment_terms": "Net 45"
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/customers/d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/customers/d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"payment_terms": "Net 45"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/customers/d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
payment_terms = "Net 45"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
PATCH https://api.epsilon3.io/v1/builds/customers/:customerId
Update an existing customer. Only the fields included in the request body are changed.
Parameters
customerId(path parameter): ID of the customer being updated.
Example Request Body
{
"payment_terms": "Net 45"
}
Request Body
name: (optional) Customer name. Must be a non-empty string of up to 255 characters.notes: (optional) Free-form notes. Must be a string of up to 1000 characters.payment_terms: (optional) Payment terms. Must be a string of up to 1000 characters, ornullto clear it.terms_and_conditions: (optional) Terms and conditions. Must be a string of up to 1000 characters, ornullto clear it.discount_bp: (optional) Default discount in basis points (100 bp = 1%). Must be an integer between 1 and 10000, ornullto clear it.archived: (optional) Whether the customer is archived. Must be a boolean.attachments: (optional) Array of attachment objects, each with anattachment_id.details: (optional) Custom detail fields, each with anidand avalueobject withrecorded.
Response
- Status:
204 No Content - Body: empty.
Sales Tax Jurisdictions
List Sales Tax Jurisdictions
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions
Retrieve all sales tax jurisdictions defined in your workspace.
Example Response Body
{
"jurisdictions": [
{
"id": "e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f",
"team_id": "teamolympus",
"code": "CA",
"name": "California",
"tax_rate_bp": 725,
"created_at": "2024-06-19T02:06:14.204Z",
"updated_at": "2024-06-19T02:06:14.204Z"
}
]
}
tax_rate_bp is the tax rate in basis points (100 bp = 1%); 725 is 7.25%.
Create Sales Tax Jurisdictions
Sample Request
import requests
import os
data = {
"jurisdictions": [
{
"name": "California",
"code": "CA",
"tax_rate_bp": 725
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"jurisdictions": [
{
"name": "California",
"code": "CA",
"tax_rate_bp": 725
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
jurisdictions = @(
@{
name = "California"
code = "CA"
tax_rate_bp = 725
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
POST https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions
Create one or more sales tax jurisdictions in your workspace. This endpoint supports bulk creation of up to 1000 jurisdictions.
Example Request Body
{
"jurisdictions": [
{
"name": "California",
"code": "CA",
"tax_rate_bp": 725
}
]
}
Example Response Body
{
"ids": ["e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f"]
}
Request Body
jurisdictions: (required) Array of jurisdiction objects to create (1 to 1000 jurisdictions). Each jurisdiction object contains:name: (required) Jurisdiction name. Must be a non-empty string of up to 255 characters.code: (required) Short code for the jurisdiction. Must be a non-empty string of up to 255 characters.tax_rate_bp: (required) Tax rate in basis points (100 bp = 1%). Must be an integer between 1 and 100000.
Response
- Status:
201 Created - Body: object with
ids, an array of system-generated IDs for the created jurisdictions, in the same order as the request.
Update Sales Tax Jurisdiction
Sample Request
import requests
import os
data = {
"tax_rate_bp": 700
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions/e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions/e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"tax_rate_bp": 700
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions/e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
tax_rate_bp = 700
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
PATCH https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions/:jurisdictionId
Update an existing sales tax jurisdiction. Only the fields included in the request body are changed.
Parameters
jurisdictionId(path parameter): ID of the jurisdiction being updated.
Example Request Body
{
"tax_rate_bp": 700
}
Request Body
name: (optional) Jurisdiction name. Must be a non-empty string of up to 255 characters.code: (optional) Short code for the jurisdiction. Must be a non-empty string of up to 255 characters.tax_rate_bp: (optional) Tax rate in basis points (100 bp = 1%). Must be an integer between 1 and 100000.
Response
- Status:
200 OK - Body: the full updated jurisdiction object (same shape as an entry in the List Sales Tax Jurisdictions response).
Delete Sales Tax Jurisdictions
Sample Request
import requests
import os
data = {
"ids": ["e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f"]
}
requests.delete(
url='https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X DELETE \
https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"ids": ["e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f"]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
ids = @("e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f")
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
DELETE https://api.epsilon3.io/v1/builds/sales-tax-jurisdictions
Delete one or more sales tax jurisdictions by ID. IDs that do not match an existing jurisdiction are returned in invalid_ids.
Example Request Body
{
"ids": ["e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f"]
}
Example Response Body
{
"deleted_ids": ["e3c4d5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f"]
}
Request Body
ids: (required) Array of jurisdiction IDs to delete.
Response
- Status:
200 OKwhen at least one jurisdiction is deleted;404 Not Foundwhen none of the requested IDs match. - Body: object with
deleted_ids(the IDs that were deleted) and, when some IDs did not match,invalid_ids(the IDs that were not found).
Tools
Deprecated: The tool-level maintenance_interval_days field is deprecated and will be removed from Get Tools, Get Tool, Create Tools and Update Tool in a future release. Until then it is still accepted and returned. To set how often a tool is serviced, use maintenance routines instead.
Get Tools
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/tools',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/tools \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/tools"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/tools
Retrieve tools defined in your workspace. The response is paginated and can be filtered and sorted using the query parameters below.
Example Response Body
{
"tools": [
{
"id": 1,
"tool_number": "TL-001",
"tracking": "serial",
"name": "Torque Wrench",
"description": "Calibrated torque wrench for engine assembly",
"image_attachment_id": "att_abc123",
"usage_type_ids": [1, 2],
"usage_declarations": [
{
"id": 10,
"name": "Run Hours",
"team_id": "k8f2n5q9w3x7z1c4v6b8m2p5r7t9y3h6",
"kind": "accumulate",
"units": "hours",
"precision": 1,
"allow_negative": false,
"allow_reset": true
}
],
"usage_limits": [
{
"usage_type_id": 10,
"limit_max": 2000,
"limit_warn_max": 1800,
"enforcement": "block"
}
],
"shelf_life_minutes": null,
"shelf_life_warn_minutes": null,
"expiry_enforcement": "warn",
"revision_ordinal": 1,
"maintenance_schedules": [
{
"id": "sched_abc",
"tool_id": 1,
"name": "Quarterly Calibration",
"type": "time",
"rrule": "FREQ=MONTHLY;INTERVAL=3",
"start_at": "2025-01-01T00:00:00.000Z",
"procedure_id": null,
"created_at": "2025-01-01T00:00:00.000Z",
"updated_at": "2025-01-01T00:00:00.000Z"
}
],
"details": [],
"created_at": "2025-01-01T00:00:00.000Z"
},
...
],
"pagination": {
"limit": 100,
"total": 342,
"next_page_token": "eyJvZmZzZXQiOjEwMH0"
}
}
Paging Parameters
limit(query parameter): optional maximum number of tools to return per page. Must be an integer between1and1000. Defaults to1000when omitted.page-token(query parameter): optional continuation token. To fetch the next page, pass back thenext_page_tokenreturned in the previous response'spaginationblock. Use the token only as returned — do not construct or parse it. Changing the filters or sort invalidates a previously returned token; restart paging from the first page when they change.sort(query parameter): optional field to sort by. Currentlytool_numberis the only supported value. When omitted, results are ordered by tool ID.order(query parameter): optional sort direction. Must be eitherascordesc. Defaults toasc. Results have a stable secondary sort by tool ID.
Filter Parameters
Each filter may be specified multiple times to match against multiple values. Within a single filter the values are combined with OR; different filters are combined with AND.
id(query parameter): optional tool ID(s) to filter to specific tools.tool-number(query parameter): optional tool number(s) to filter to specific tools. Matching is case-insensitive.tag-id(query parameter): optional tag ID(s) to return only tools associated with those tags.maintenance-status(query parameter): optional maintenance status(es). Each value must be one ofoverdueorapproaching. Returns only tools with at least one instance in the given maintenance state.has-checked-out(query parameter): optional boolean. Set totrueto return only tools that have at least one checked-out instance, orfalseto return only tools with none checked out.has-available(query parameter): optional boolean. Set totrueto return only tools that have at least one available instance, orfalseto return only tools with none available.usage-type-id(query parameter): optional usage type ID(s). Returns only tools that declare every listed usage type. Unlike the other filters, its values are combined with AND.
Include Parameters
include-instance-summary(query parameter): optional boolean. Set totrueto addinstance_summaryto each tool.
Query parameter example usage
GET https://api.epsilon3.io/v1/builds/tools?limit=100&maintenance-status=overdue&has-available=true
Response fields
tools: The tools on this page. Each tool includes:usage_type_ids: The tool's legacy usage type IDs. Seeusage_declarationsfor the usage types its instances record.usage_declarations: The usage types the tool declares, so its instances can record usage of them. Same shape as List Usage Types. See Usage.usage_limits: The tool's usage limits, each withusage_type_id,enforcement, and whichever oflimit_min,limit_maxandlimit_warn_maxare set. Declared types without a limit are left out.shelf_life_minutesandshelf_life_warn_minutes: The tool's shelf life, ornullwhen it has none.expiry_enforcement:allow,warnorblock.instance_summary: Only withinclude-instance-summary=true. Counts across the tool's active instances:instance_count,checked_out_countandavailable_count: a lot instance counts as its quantity and a serial instance as one.overdue_countandapproaching_count: instances whose maintenance is overdue or approaching. An instance is overdue when one of its maintenance statuses isdueoroverdue, and approaching when one isdue_soonand none is past due.
pagination: Paging metadata for the response.limit: The page size that was applied.total: Total number of tools matching the filters across all pages.next_page_token: Continuation token for retrieving the next page, ornullwhen this is the last page.
Get Tool
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/tools/1',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/tools/1 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/tools/1"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
GET https://api.epsilon3.io/v1/builds/tools/:toolId
Retrieve a single tool using its ID. Optionally retrieve a specific revision.
Parameters
toolId(path parameter): ID of the tool being retrieved.revisionId(query parameter, optional): ID of a specific revision to retrieve.
Example Response Body
{
"id": 1,
"tool_number": "TL-001",
"tracking": "serial",
"name": "Torque Wrench",
"description": "Calibrated torque wrench for engine assembly",
"image_attachment_id": "att_abc123",
"usage_type_ids": [1, 2],
"usage_declarations": [
{
"id": 10,
"name": "Run Hours",
"team_id": "k8f2n5q9w3x7z1c4v6b8m2p5r7t9y3h6",
"kind": "accumulate",
"units": "hours",
"precision": 1,
"allow_negative": false,
"allow_reset": true
}
],
"usage_limits": [
{
"usage_type_id": 10,
"limit_max": 2000,
"limit_warn_max": 1800,
"enforcement": "block"
}
],
"shelf_life_minutes": null,
"shelf_life_warn_minutes": null,
"expiry_enforcement": "warn",
"revision_ordinal": 1,
"maintenance_schedules": [
{
"id": "sched_abc",
"tool_id": 1,
"name": "Quarterly Calibration",
"type": "time",
"rrule": "FREQ=MONTHLY;INTERVAL=3",
"start_at": "2025-01-01T00:00:00.000Z",
"procedure_id": null,
"created_at": "2025-01-01T00:00:00.000Z",
"updated_at": "2025-01-01T00:00:00.000Z"
}
],
"details": [],
"instance_summary": {
"instance_count": 7,
"checked_out_count": 2,
"available_count": 4,
"overdue_count": 1,
"approaching_count": 1
},
"created_at": "2025-01-01T00:00:00.000Z"
}
Response fields
The tool has the same fields as on Get Tools, and always includes instance_summary.
Create Tools
Sample Request (Single Tool)
import requests
import os
data = {
"tools": [
{
"tool_number": "TL-002",
"name": "Multimeter",
"description": "Digital multimeter for electrical testing",
"usage_type_ids": [1]
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/tools',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/tools \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"tools": [
{
"tool_number": "TL-002",
"name": "Multimeter",
"description": "Digital multimeter for electrical testing",
"usage_type_ids": [1]
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/tools"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
tools = @(
@{
tool_number = "TL-002"
name = "Multimeter"
description = "Digital multimeter for electrical testing"
usage_type_ids = @(1)
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body (Single Tool)
{
"tools": [
{
"tool_number": "TL-002",
"name": "Multimeter",
"description": "Digital multimeter for electrical testing",
"usage_type_ids": [1]
}
]
}
Example Request Body (Multiple Tools)
{
"tools": [
{
"tool_number": "TL-002",
"name": "Multimeter",
"description": "Digital multimeter for electrical testing",
"usage_type_ids": [1]
},
{
"tool_number": "TL-003",
"name": "Oscilloscope",
"usage_type_ids": []
}
]
}
Example Response Body
{
"ids": [2, 3]
}
POST https://api.epsilon3.io/v1/builds/tools
Create one or more tools in your workspace. This endpoint supports bulk creation of tools.
Request Body
tools: (required) Array of tool objects to create (1 to 1000 tools). Each tool object contains:tool_number: (required) Unique tool number. Must be a string of up to 255 characters.name: (required) Tool name. Must be a string of up to 255 characters.description: (optional) Tool description. Must be a string of up to 4096 characters.image_attachment_id: (optional) System generated ID of an uploaded image attachment.usage_type_ids: (optional) Array of 0 to 2 IDs of the built-incyclesandtimeusage types, from List Usage Types, for legacy tool usage tracking. These are separate fromusage_declaration_ids, which sets the usage types the tool's instances record.tracking: (optional) How the tool's instances are tracked. Must beserial(each instance is one tool with a serial number) orlot(each instance is a lot of identical tools with a lot number and a quantity). Defaults toserial.details: (optional) Custom tool detail fields. Must be an array of 0 to 100 objects with:id: (required) ID of the custom field.value: (required) Object containing:recorded: (required) The value to set for the custom field.
usage_declaration_ids: (optional) Array of 0 to 50 IDs of usage types the tool declares, so its instances can record usage of them. See Usage. Archived usage types cannot be declared.usage_limits: (optional) Array of 0 to 50 life limits on declared usage types. Each object has:usage_type_id: (required) ID of a usage type the tool declares.limit_max: (optional) For anaccumulatetype, the total since the latest reset at which an instance is at its limit. For asnapshottype, the highest reading an instance may have.limit_warn_max: (optional)accumulatetypes only. The total at which an instance is near its limit. Must be belowlimit_max.limit_min: (optional)snapshottypes only. The lowest reading an instance may have. Cannot be abovelimit_max.enforcement: (optional) What happens when an instance at its limit is used, for example checked out:allow,warn(the use goes ahead with a warning) orblock(the use is refused). Defaults towarn.categoricaltypes cannot take limits. A limit that breaks these rules, or names a usage type the tool does not declare, returns400.shelf_life_minutes: (optional) How long the tool's instances last, in whole minutes from 1 to 52,560,000 (100 years). An instance created without anexpires_atexpires this long after it is created.shelf_life_warn_minutes: (optional) How many minutes before it expires an instance counts as expiring soon, from 1 to 52,560,000. Requiresshelf_life_minutesand must be less than it; otherwise the request returns400.expiry_enforcement: (optional) What happens when an expired instance is used:allow,warnorblock. Defaults towarn.
Response
The response status code will be 200 and return an array of IDs for the created tools.
Update Tool
Sample Request
import requests
import os
data = {
"name": "Torque Wrench (Updated)"
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/tools/1',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/tools/1 \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"name": "Torque Wrench (Updated)"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/tools/1"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
name = "Torque Wrench (Updated)"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"name": "Torque Wrench (Updated)"
}
PATCH https://api.epsilon3.io/v1/builds/tools/:toolId
Updates values of one or more fields for a specific tool.
Parameters
toolId(path parameter): ID of the tool to update.
Request Body
The request body should be a JSON object that contains the specific fields of the tool to be updated. The request body must contain at least one field. Following fields are supported:
tool_number: (optional) Unique tool number. Must be a string of up to 255 characters.name: (optional) Tool name. Must be a string of up to 255 characters.description: (optional) Tool description. Must be a string of up to 4096 characters, ornullto clear.image_attachment_id: (optional) System generated ID of an uploaded image attachment, ornullto clear.usage_type_ids: (optional) Array of 0 to 2 IDs of the built-incyclesandtimeusage types, for legacy tool usage tracking. These are separate fromusage_declaration_ids.tracking: (optional)serialorlot. Can only change while the tool has no instances, including archived ones; otherwise the request returns422.details: (optional) Custom tool detail fields. Must be an array of 0 to 100 objects with:id: (required) ID of the custom field.value: (required) Object containing:recorded: (required) The value to set for the custom field.
usage_declaration_ids: (optional) Array of 0 to 50 IDs of usage types the tool declares, so its instances can record usage of them. See Usage.- The array replaces the tool's declarations. Specify an empty array to remove them all; leave the field out to keep them.
- Removing a declaration also removes its usage limit.
- Archived usage types cannot be newly declared.
usage_limits: (optional) Array of 0 to 50 life limits on declared usage types, in the same format as on Create Tools.- The array replaces the tool's limits: a declared usage type left out loses its limit. Specify an empty array to remove them all; leave the field out to keep them.
- A limit can name a usage type declared in the same request.
shelf_life_minutes: (optional) How long the tool's instances last, in whole minutes from 1 to 52,560,000 (100 years), ornullto clear. To clear a shelf life that has a warning, also send"shelf_life_warn_minutes": nullin the same request; otherwise the request returns400. Changing it does not change instances'expires_atunlessupdate_instance_expirationsis set.shelf_life_warn_minutes: (optional) How many minutes before it expires an instance counts as expiring soon, from 1 to 52,560,000, ornullto clear. It must be less than the shelf life, and cannot be set without one; otherwise the request returns400.expiry_enforcement: (optional) What happens when an expired instance is used:allow,warnorblock.update_instance_expirations: (optional) Set totrueto resetexpires_aton every active instance of the tool to the instance's creation time plus the tool's shelf life after this update. This overwrites expirations set by hand, and clears them when the tool has no shelf life.
Response
- Status:
204 No Content - Body: (empty)
Delete Tools
Sample Request
import requests
import os
data = {
"ids": [2, 3]
}
requests.delete(
url='https://api.epsilon3.io/v1/builds/tools',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X DELETE \
https://api.epsilon3.io/v1/builds/tools \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"ids": [2, 3]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/tools"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
ids = @(2, 3)
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"ids": [2, 3]
}
DELETE https://api.epsilon3.io/v1/builds/tools
Delete one or more tools and all associated issue references.
Request Body
ids: (required) Array of tool IDs to delete (1 to 1000 IDs).
Response
- Status:
204 No Content - Body: (empty)
Tool Instances
Get Tool Instances
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/tools/instances',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/tools/instances \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/tools/instances"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/tools/instances
Retrieve tool instances defined in your workspace. The response is paginated and can be filtered using the query parameters below.
Example Response Body
{
"tool_instances": [
{
"id": 1,
"tool_id": 1,
"serial_number": "SN-001",
"tracking": "serial",
"quantity": 1,
"checked_out_quantity": 0,
"notes": "Located in Lab A",
"status": "available",
"attachments": [
{
"attachment_id": "att_xyz",
"name": "calibration_cert.pdf",
"content_type": "application/pdf"
}
],
"checked_out": null,
"check_in_outs": [],
"usage": [],
"maintenance_schedules": [],
"latest_maintenance_events": [],
"details": [],
"archived_at": null,
"expires_at": "2026-01-15T00:00:00.000Z",
"shelf_life_warn_minutes": 43200,
"maintenance_status_summaries": [
{
"due_state": "due_soon",
"routine_id": "1a24067f-67d1-40ff-a301-c0269a825cc1",
"routine_name": "Torque Wrench Calibration",
"trigger_name": "Annual calibration",
"next_due_at": "2025-12-01T00:00:00.000Z",
"should_block_use_when_due": true
}
],
"usage_limit_count": 1,
"created_at": "2025-01-15T00:00:00.000Z",
"updated_at": "2025-01-15T00:00:00.000Z"
},
...
],
"pagination": {
"limit": 100,
"total": 512,
"next_page_token": "eyJvZmZzZXQiOjEwMH0"
}
}
Paging Parameters
limit(query parameter): optional maximum number of tool instances to return per page. Must be an integer between1and500. Defaults to500when omitted.page-token(query parameter): optional continuation token. To fetch the next page, pass back thenext_page_tokenreturned in the previous response'spaginationblock. Use the token only as returned — do not construct or parse it. Changing the filters or sort invalidates a previously returned token; restart paging from the first page when they change.sort(query parameter): optional field to sort by. Must be one oftool_number,serial_number, orexpires_at. When omitted, results are ordered by tool instance ID. Tool instances without an expiration sort last when sorting byexpires_at, in either direction.order(query parameter): optional sort direction. Must be eitherascordesc. Defaults toasc. Results have a stable secondary sort by tool instance ID.
Filter Parameters
Each filter may be specified multiple times to match against multiple values. Within a single filter the values are combined with OR; different filters are combined with AND.
tool-id(query parameter): optional tool ID(s) to return only instances of those tools.id(query parameter): optional tool instance ID(s) to filter to specific instances.state(query parameter): optional instance state(s). Each value must be one ofavailable,unavailable, orchecked_out. These states are mutually exclusive: an instance that is checked out ischecked_outregardless of its stored status.location-id(query parameter): optional location ID(s) to return only instances at those locations.maintenance-due(query parameter): optional maintenance status(es). Each value must be one ofoverdueorapproaching.archived-status(query parameter): optional archive state(s). Each value must be one ofactiveorarchived. To pass both, repeat the parameter (archived-status=active&archived-status=archived); a comma-separated list is rejected. When omitted, all tool instances are returned, active and archived.alert(query parameter): optional alert(s). Returns instances with any of the given alerts. Each value must be one of:expired: the instance'sexpires_athas passed.expiring_soon: the instance expires within its tool'sshelf_life_warn_minutes.at_limit: the instance is at one of its tool's usage limits.near_limit: the instance is near one of its tool's usage limits, and at none.due_soon,due,overdue: the instance has a maintenance status in that due state.
usage-type-id(query parameter): optional usage type ID(s). Returns only instances whose tool declares every listed usage type. Unlike the other filters, its values are combined with AND.
Query parameter example usage
GET https://api.epsilon3.io/v1/builds/tools/instances?tool-id=1&state=checked_out&maintenance-due=overdue&archived-status=active&sort=serial_number&order=desc&limit=100
Response fields
tool_instances: The tool instances on this page.status: Instance status, eitheravailableorunavailable.archived_at: ISO 8601 timestamp of when the tool instance was archived, ornullwhen it is active.expires_at: ISO 8601 timestamp of when the instance expires. Omitted when it has no expiration.shelf_life_warn_minutes: The tool'sshelf_life_warn_minutes, which sets when the instance counts as expiring soon. Omitted when the tool has none.maintenance_status_summaries: The instance's maintenance statuses, most urgent first. Each hasdue_state,routine_id,routine_name,trigger_name,should_block_use_when_dueand, when it has one,next_due_at. Omitted when the instance has none.usage_limit_count: The number of usage limits set on the instance's tool. Omitted when it has none.usage_limit_alerts: The usage limits the instance is near or at, in the same shape as on Get Inventory Item. Omitted when there are none.
pagination: Paging metadata for the response.limit: The page size that was applied.total: Total number of tool instances matching the filters across all pages.next_page_token: Continuation token for retrieving the next page, ornullwhen this is the last page.
Get Tool Instance
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/tools/1/instances/1',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/tools/1/instances/1 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/tools/1/instances/1"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
GET https://api.epsilon3.io/v1/builds/tools/:toolId/instances/:toolInstanceId
Retrieve a single tool instance.
Parameters
toolId(path parameter): ID of the tool.toolInstanceId(path parameter): ID of the tool instance being retrieved.
Example Response Body
{
"id": 1,
"tool_id": 1,
"serial_number": "SN-001",
"tracking": "serial",
"quantity": 1,
"checked_out_quantity": 0,
"notes": "Located in Lab A",
"status": "available",
"attachments": [
{
"attachment_id": "att_xyz",
"name": "calibration_cert.pdf",
"content_type": "application/pdf"
}
],
"checked_out": null,
"check_in_outs": [],
"usage": [],
"maintenance_schedules": [],
"latest_maintenance_events": [],
"details": [],
"archived_at": null,
"expires_at": "2026-01-15T00:00:00.000Z",
"maintenance_status_summaries": [
{
"due_state": "due_soon",
"routine_id": "1a24067f-67d1-40ff-a301-c0269a825cc1",
"routine_name": "Torque Wrench Calibration",
"trigger_name": "Annual calibration",
"next_due_at": "2025-12-01T00:00:00.000Z",
"should_block_use_when_due": true
}
],
"usage_limit_count": 1,
"usage_summaries": [
{
"usage_type_id": 10,
"usage_type_name": "Run Hours",
"usage_type_kind": "accumulate",
"usage_type_units": "hours",
"usage_type_precision": 1,
"latest_recorded_at": "2025-03-02T10:15:00.000Z",
"reading_count": 9,
"latest_value": 4.5,
"total_value": 212.5,
"reading_count_since_reset": 9
}
],
"created_at": "2025-01-15T00:00:00.000Z",
"updated_at": "2025-01-15T00:00:00.000Z"
}
Response fields
status: Instance status, eitheravailableorunavailable.archived_at: ISO 8601 timestamp of when the tool instance was archived, ornullwhen it is active.expires_at,maintenance_status_summaries,usage_limit_countandusage_limit_alerts: as on Get Tool Instances.usage_summaries: The instance's current usage, one entry per usage type it has readings for. Same shape as List Usage Summaries, without the subject ID.
Create Tool Instances
Sample Request
import requests
import os
data = {
"tool_instances": [
{
"tool_id": 1,
"serial_number": "SN-002",
"notes": "New unit for Lab B",
"attachments": []
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/tools/instances',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/tools/instances \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"tool_instances": [
{
"tool_id": 1,
"serial_number": "SN-002",
"notes": "New unit for Lab B",
"attachments": []
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/tools/instances"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
tool_instances = @(
@{
tool_id = 1
serial_number = "SN-002"
notes = "New unit for Lab B"
attachments = @()
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"tool_instances": [
{
"tool_id": 1,
"serial_number": "SN-002",
"notes": "New unit for Lab B",
"attachments": []
}
]
}
Example Response Body
{
"ids": [2]
}
POST https://api.epsilon3.io/v1/builds/tools/instances
Create one or more tool instances in your workspace. This endpoint supports bulk creation of tool instances.
Request Body
tool_instances: (required) Array of tool instance objects to create (1 to 1000 instances). Each tool instance object contains:tool_id: (required) ID of the parent tool definition.serial_number: (required) Unique serial number, or the lot number for a lot tracked tool. Must be a string of up to 255 characters.quantity: (optional) Number of tools in the lot, a whole number from 1 to 1,000,000. Defaults to 1. Must be 1 (or omitted) for a serial tracked tool.notes: (optional) Instance notes. Must be a string of up to 4096 characters.location_id: (optional) ID of the inventory location.status: (optional) Instance status. Must be one of:available,unavailable.attachments: (required) Array of attachment objects (0 to 1000 items). Each object contains:attachment_id: (required) System generated ID of the uploaded attachment.details: (optional) Custom instance detail fields. Must be an array of 0 to 100 objects with:id: (required) ID of the custom field.value: (required) Object containing:recorded: (required) The value to set for the custom field.
expires_at: (optional) When the instance expires. Must be an ISO 8601 date-time with a time zone offset, such as2026-12-31T00:00:00Z, ornullfor no expiration. When left out, the instance expires the tool'sshelf_life_minutesafter it is created, or never when the tool has no shelf life.
Response
The response status code will be 200 and return an array of IDs for the created tool instances.
Update Tool Instance
Sample Request
import requests
import os
data = {
"notes": "Moved to Lab C",
"status": "unavailable"
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/tools/1/instances/1',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/tools/1/instances/1 \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"notes": "Moved to Lab C",
"status": "unavailable"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/tools/1/instances/1"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
notes = "Moved to Lab C"
status = "unavailable"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"notes": "Moved to Lab C",
"status": "unavailable"
}
PATCH https://api.epsilon3.io/v1/builds/tools/:toolId/instances/:toolInstanceId
Updates values of one or more fields for a specific tool instance.
Parameters
toolId(path parameter): ID of the tool.toolInstanceId(path parameter): ID of the tool instance to update.
Request Body
The request body should be a JSON object that contains the specific fields of the tool instance to be updated. The request body must contain at least one field. Following fields are supported:
notes: (optional) Instance notes. Must be a string of up to 4096 characters, ornullto clear.quantity: (optional) Number of tools in a lot tracked instance, a whole number from 1 to 1,000,000. It cannot drop below the number currently checked out, and a serial tracked instance cannot change it.location_id: (optional) ID of the inventory location, ornullto clear.status: (optional) Instance status. Must be one of:available,unavailable.attachments: (optional) Array of attachment objects (0 to 1000 items). Each object contains:attachment_id: (required) System generated ID of the uploaded attachment.
details: (optional) Custom instance detail fields. Must be an array of 0 to 100 objects with:id: (required) ID of the custom field.value: (required) Object containing:recorded: (required) The value to set for the custom field.
archived: (optional) Whether the tool instance is archived. Must be a boolean. Setting it requires thebuilds:edit(Builds Edit) permission when authenticating with a user API key. A checked-out tool instance cannot be archived; the request returns422until it is checked in.expires_at: (optional) When the instance expires. Must be an ISO 8601 date-time with a time zone offset, such as2026-12-31T00:00:00Z. Set tonullto remove the expiration.
An archived tool instance must be unarchived (by setting archived to false) before other fields can be changed. A request that changes any other field of an archived tool instance returns 400, even if it also sets archived to false.
Response
- Status:
204 No Content - Body: (empty)
Delete Tool Instances
Sample Request
import requests
import os
data = {
"ids": [2, 3]
}
requests.delete(
url='https://api.epsilon3.io/v1/builds/tools/instances',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X DELETE \
https://api.epsilon3.io/v1/builds/tools/instances \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"ids": [2, 3]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/tools/instances"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
ids = @(2, 3)
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"ids": [2, 3]
}
DELETE https://api.epsilon3.io/v1/builds/tools/instances
Delete one or more tool instances.
Request Body
ids: (required) Array of tool instance IDs to delete (1 to 1000 IDs).
Response
- Status:
204 No Content - Body: (empty)
List Tool Instance Activity
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/tools/1/instances/1/activity',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/tools/1/instances/1/activity \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/tools/1/instances/1/activity"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/tools/:toolId/instances/:toolInstanceId/activity
Retrieve the activity history for a tool instance. Results are paginated using cursor-based pagination.
Parameters
toolId(path parameter): ID of the tool.toolInstanceId(path parameter): ID of the tool instance.startBeforeId(query parameter, optional): ID of the activity to start pagination before, for fetching older records.
Example Response Body
{
"has_older": false,
"activities": [
{
"id": "act_abc",
"timestamp": "2025-02-01T12:00:00.000Z",
"tool_instance_id": 1,
"action": "Maintenance Completed",
"actor_type": "user",
"actor_id": "user_123"
}
]
}
Check Out Tool Instances
Sample Request
import requests
import os
data = {
"tool_instance_ids": [2, 3]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/tool-check-outs',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/tool-check-outs \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"tool_instance_ids": [2, 3]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/tool-check-outs"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
tool_instance_ids = @(2, 3)
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"tool_instance_ids": [2, 3]
}
POST https://api.epsilon3.io/v1/builds/tool-check-outs
Check one or more tool instances out directly, outside of a procedure run. Every instance must be available (not scrapped) and not already checked out. The request is all-or-nothing: if any instance is missing, unavailable, or already checked out, no instances are checked out.
Request Body
tool_instance_ids: (required) Array of tool instance IDs to check out (1 to 1000 IDs). Each ID must be a positive integer and unique within the request.quantities: (optional) Array of{ "tool_instance_id", "quantity" }objects giving how many tools to check out of a lot tracked instance. An instance without an entry moves 1. A lot cannot check out more than it has available, and a serial tracked instance must move exactly 1. A lot stays available for more check outs while some of it is left.
Response
- Status:
204 No Content - Body: (empty)
Check In Tool Instances
Sample Request
import requests
import os
data = {
"tool_instance_ids": [2, 3]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/tool-check-ins',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/tool-check-ins \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"tool_instance_ids": [2, 3]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/tool-check-ins"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
tool_instance_ids = @(2, 3)
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"tool_instance_ids": [2, 3]
}
POST https://api.epsilon3.io/v1/builds/tool-check-ins
Check one or more tool instances back in directly, outside of a procedure run. Every instance must currently be checked out. The request is all-or-nothing: if any instance is missing or is not currently checked out, no instances are checked in.
Request Body
tool_instance_ids: (required) Array of tool instance IDs to check in (1 to 1000 IDs). Each ID must be a positive integer and unique within the request.quantities: (optional) Array of{ "tool_instance_id", "quantity" }objects giving how many tools to check back in to a lot tracked instance. An instance without an entry moves 1. A lot cannot check in more than it has checked out, and a serial tracked instance must move exactly 1.
Response
- Status:
204 No Content - Body: (empty)
Tool Maintenance Schedules
Deprecated: These endpoints are deprecated and will be removed in a future release. To schedule maintenance, use maintenance routines instead of tool maintenance schedules, and record maintenance with Create Maintenance Events instead of Create Tool Maintenance Events.
Create Tool Maintenance Schedule
Sample Request
import requests
import os
data = {
"name": "Monthly Calibration",
"type": "time",
"start_at": "2025-03-01T00:00:00.000Z",
"rrule": "FREQ=MONTHLY;INTERVAL=1"
}
requests.post(
url='https://api.epsilon3.io/v1/builds/tools/1/maintenance/schedules',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/tools/1/maintenance/schedules \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"name": "Monthly Calibration",
"type": "time",
"start_at": "2025-03-01T00:00:00.000Z",
"rrule": "FREQ=MONTHLY;INTERVAL=1"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/tools/1/maintenance/schedules"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
name = "Monthly Calibration"
type = "time"
start_at = "2025-03-01T00:00:00.000Z"
rrule = "FREQ=MONTHLY;INTERVAL=1"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body (Time-based)
{
"name": "Monthly Calibration",
"type": "time",
"start_at": "2025-03-01T00:00:00.000Z",
"rrule": "FREQ=MONTHLY;INTERVAL=1"
}
Example Request Body (Check-out-based)
{
"name": "Every 50 Check-Outs",
"type": "check_outs",
"start_at": "2025-03-01T00:00:00.000Z",
"check_outs_interval": 50
}
Example Request Body (Usage-based)
{
"name": "Every 100 Hours",
"type": "usage",
"start_at": "2025-03-01T00:00:00.000Z",
"usage_interval": 100,
"usage_type_id": 1
}
POST https://api.epsilon3.io/v1/builds/tools/:toolId/maintenance/schedules
Create a maintenance schedule for a tool. The schedule applies to all instances of the tool.
Parameters
toolId(path parameter): ID of the tool.
Request Body
name: (required) Schedule name. Must be a string.type: (required) Schedule type. Must be one of:time,check_outs,usage.start_at: (required) Schedule start date in ISO 8601 format.procedure_id: (optional) ID of a procedure to associate with the schedule.rrule: (optional) Recurrence rule string (RFC 5545 RRULE format). Only applicable when type istime.check_outs_interval: (conditional) Number of check-outs between maintenance. Required when type ischeck_outs. Must be between 1 and 9,999,999,999.usage_interval: (conditional) Usage amount between maintenance. Required when type isusage. Must be between 1 and 9,999,999,999.usage_type_id: (conditional) ID of the usage type. Required when type isusage. Must be a positive number.
Response
- Status:
204 No Content - Body: (empty)
Create Tool Instance Maintenance Schedule
Sample Request
import requests
import os
data = {
"name": "Instance-Specific Inspection",
"type": "time",
"start_at": "2025-06-01T00:00:00.000Z",
"rrule": "FREQ=WEEKLY;INTERVAL=2"
}
requests.post(
url='https://api.epsilon3.io/v1/builds/tools/1/instances/1/maintenance/schedules',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/tools/1/instances/1/maintenance/schedules \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"name": "Instance-Specific Inspection",
"type": "time",
"start_at": "2025-06-01T00:00:00.000Z",
"rrule": "FREQ=WEEKLY;INTERVAL=2"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/tools/1/instances/1/maintenance/schedules"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
name = "Instance-Specific Inspection"
type = "time"
start_at = "2025-06-01T00:00:00.000Z"
rrule = "FREQ=WEEKLY;INTERVAL=2"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"name": "Instance-Specific Inspection",
"type": "time",
"start_at": "2025-06-01T00:00:00.000Z",
"rrule": "FREQ=WEEKLY;INTERVAL=2"
}
POST https://api.epsilon3.io/v1/builds/tools/:toolId/instances/:toolInstanceId/maintenance/schedules
Create a maintenance schedule for a specific tool instance only.
Parameters
toolId(path parameter): ID of the tool.toolInstanceId(path parameter): ID of the tool instance.
Request Body
name: (required) Schedule name. Must be a string.type: (required) Schedule type. Must be one of:time,check_outs,usage.start_at: (required) Schedule start date in ISO 8601 format.procedure_id: (optional) ID of a procedure to associate with the schedule.rrule: (optional) Recurrence rule string (RFC 5545 RRULE format). Only applicable when type istime.check_outs_interval: (conditional) Number of check-outs between maintenance. Required when type ischeck_outs. Must be between 1 and 9,999,999,999.usage_interval: (conditional) Usage amount between maintenance. Required when type isusage. Must be between 1 and 9,999,999,999.usage_type_id: (conditional) ID of the usage type. Required when type isusage. Must be a positive number.
Response
- Status:
204 No Content - Body: (empty)
Update Tool Maintenance Schedule
Sample Request
import requests
import os
data = {
"name": "Bi-Monthly Calibration",
"rrule": "FREQ=MONTHLY;INTERVAL=2"
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/tools/maintenance/schedules/sched_abc',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/tools/maintenance/schedules/sched_abc \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"name": "Bi-Monthly Calibration",
"rrule": "FREQ=MONTHLY;INTERVAL=2"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/tools/maintenance/schedules/sched_abc"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
name = "Bi-Monthly Calibration"
rrule = "FREQ=MONTHLY;INTERVAL=2"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"name": "Bi-Monthly Calibration",
"rrule": "FREQ=MONTHLY;INTERVAL=2"
}
PATCH https://api.epsilon3.io/v1/builds/tools/maintenance/schedules/:maintenanceScheduleId
Update an existing maintenance schedule.
Parameters
maintenanceScheduleId(path parameter): ID of the maintenance schedule to update.
Request Body
The request body should be a JSON object that contains the specific fields to update. Following fields are supported:
name: (optional) Schedule name.start_at: (optional) Schedule start date in ISO 8601 format.procedure_id: (optional) ID of a procedure, ornullto clear.type: (optional) Schedule type. Must be one of:time,check_outs,usage.rrule: (optional) Recurrence rule string, ornullto clear. Only applicable when type istime.check_outs_interval: (optional) Number of check-outs between maintenance, ornullto clear. Must be between 1 and 9,999,999,999.usage_interval: (optional) Usage amount between maintenance, ornullto clear. Must be between 1 and 9,999,999,999.usage_type_id: (optional) ID of the usage type, ornullto clear.
Response
- Status:
204 No Content - Body: (empty)
Returns 422 Unprocessable Entity if the schedule configuration is invalid.
Delete Tool Maintenance Schedule
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/builds/tools/maintenance/schedules/sched_abc',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X DELETE \
https://api.epsilon3.io/v1/builds/tools/maintenance/schedules/sched_abc \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/tools/maintenance/schedules/sched_abc"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers @{
"Authorization" = "Basic $authHeader"
}
DELETE https://api.epsilon3.io/v1/builds/tools/maintenance/schedules/:maintenanceScheduleId
Delete a maintenance schedule.
Parameters
maintenanceScheduleId(path parameter): ID of the maintenance schedule to delete.
Response
- Status:
204 No Content - Body: (empty)
Create Tool Maintenance Events
Sample Request
import requests
import os
data = {
"tool_maintenance_schedule_id": "sched_abc",
"tool_instance_ids": [1, 2],
"notes": "Routine calibration completed"
}
requests.post(
url='https://api.epsilon3.io/v1/builds/tools/1/instances/maintenance/events',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/tools/1/instances/maintenance/events \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"tool_maintenance_schedule_id": "sched_abc",
"tool_instance_ids": [1, 2],
"notes": "Routine calibration completed"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/tools/1/instances/maintenance/events"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
tool_maintenance_schedule_id = "sched_abc"
tool_instance_ids = @(1, 2)
notes = "Routine calibration completed"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"tool_maintenance_schedule_id": "sched_abc",
"tool_instance_ids": [1, 2],
"notes": "Routine calibration completed"
}
POST https://api.epsilon3.io/v1/builds/tools/:toolId/instances/maintenance/events
Record maintenance event(s) for one or more tool instances. Events can be linked to a procedure run or recorded as standalone maintenance.
Parameters
toolId(path parameter): ID of the tool.
Request Body
tool_maintenance_schedule_id: (required) ID of the maintenance schedule this event fulfills.tool_instance_ids: (required) Array of tool instance IDs to record maintenance for (1 to 1000 IDs).notes: (optional) Notes about the maintenance performed.maintained_by: (optional) ID or identifier of the user who performed the maintenance. Must be a string of up to 255 characters. Must be provided together withmaintained_at.maintained_at: (optional) Date and time the maintenance was performed in ISO 8601 format. Must be provided together withmaintained_by.run_id: (optional) ID of a procedure run to link to this maintenance event. Only valid whentool_instance_idscontains a single instance.
Response
- Status:
201 Created - Body: (empty)
Tool History
Get Tool History
Sample Request
import requests
import os
data = {
"tool_instance_ids": [1, 2],
"start_at": "2025-01-01T00:00:00.000Z",
"end_at": "2025-03-01T00:00:00.000Z"
}
requests.post(
url='https://api.epsilon3.io/v1/builds/tools/history',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/tools/history \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"tool_instance_ids": [1, 2],
"start_at": "2025-01-01T00:00:00.000Z",
"end_at": "2025-03-01T00:00:00.000Z"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/tools/history"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
tool_instance_ids = @(1, 2)
start_at = "2025-01-01T00:00:00.000Z"
end_at = "2025-03-01T00:00:00.000Z"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"tool_instance_ids": [1, 2],
"start_at": "2025-01-01T00:00:00.000Z",
"end_at": "2025-03-01T00:00:00.000Z"
}
Example Response Body
[
["instance_id", "timestamp", "action", "value"],
[1, "2025-01-15T10:00:00.000Z", "check_out", "run_abc"],
[1, "2025-01-15T14:00:00.000Z", "check_in", ""],
[2, "2025-02-01T08:00:00.000Z", "maintenance", "Calibration"]
]
POST https://api.epsilon3.io/v1/builds/tools/history
Retrieve tool instance history data in CSV array format for a given time range.
Request Body
tool_instance_ids: (required) Array of tool instance IDs (1 to 1000 IDs).start_at: (optional) Start date in ISO 8601 format.end_at: (optional) End date in ISO 8601 format.
Response
The response status code will be 200 and return an array of arrays containing the history data in CSV format.
Usage
Usage records how much a tool instance, an inventory item or a location has been used. Readings feed the usage limits set on parts, tools and locations, and the usage triggers of maintenance routines.
An unsupported query parameter or top-level body field returns 422.
Usage type kinds
accumulate(Running Total): each reading adds to a total, such as hours run or kilometers driven. The total counts from the latest reset.snapshot(Current Measurement): each reading is a measurement of its own, such as a pressure or a temperature. Limits and triggers read the latest value.categorical(Category): each reading picks one of the type's options, such as an oil color. A categorical type has no units, precision, negative values or resets.
Every workspace also has two built-in accumulate types, cycles and time (in seconds). They have no team_id, cannot be updated, and no other usage type can take their names.
Declaring usage types
A reading can only be recorded against a usage type its subject declares:
- An inventory item records the types declared on its part revision. Set them with
usage_declaration_idson Create Parts and Update Part. - A tool instance records the types declared on its tool. Set them with
usage_declaration_idson Create Tools and Update Tool. - A location declares its own types. Set them with
usage_declaration_idson Update Location.
The same endpoints take usage_limits, which put a life limit on a declared type. A reading always belongs to one tool instance, item or location, never to a tool or a part, so usage is tracked separately for each one.
Readings and summaries
Readings and summaries are two views of the same usage. List Usage Readings returns every reading as it was recorded, including resets and the run each one was recorded in. List Usage Summaries returns each subject's current usage, worked out from those readings: the total since the latest reset, the latest value and the reading counts. Usage limits and usage triggers check against the summary.
List Usage Types
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/usage/types',
params={'include-archived': 'true'},
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
"https://api.epsilon3.io/v1/builds/usage/types?include-archived=true" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/usage/types?include-archived=true"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
Example Response Body
{
"data": [
{
"id": 1,
"name": "cycles",
"kind": "accumulate",
"allow_negative": false,
"allow_reset": true
},
{
"id": 8,
"name": "Kilometers",
"team_id": "k8f2n5q9w3x7z1c4v6b8m2p5r7t9y3h6",
"kind": "accumulate",
"units": "km",
"precision": 1,
"allow_negative": false,
"allow_reset": true
},
{
"id": 7,
"name": "Max Altitude",
"team_id": "k8f2n5q9w3x7z1c4v6b8m2p5r7t9y3h6",
"kind": "snapshot",
"units": "ft",
"precision": 0,
"allow_negative": false,
"allow_reset": true
},
{
"id": 9,
"name": "Oil Color",
"team_id": "k8f2n5q9w3x7z1c4v6b8m2p5r7t9y3h6",
"kind": "categorical",
"options": [
{ "id": 6, "label": "Clear", "position": 0 },
{ "id": 7, "label": "Amber", "position": 1 },
{ "id": 8, "label": "Dark Amber", "position": 2 },
{ "id": 1, "label": "Yellow", "position": 3, "archived_at": "2026-09-24T00:08:12.921Z" }
]
},
{
"id": 2,
"name": "time",
"kind": "accumulate",
"units": "seconds",
"allow_negative": false,
"allow_reset": true
}
],
"pagination": {
"limit": 1000,
"sort_by": null,
"sort_order": null,
"group_by": null,
"current_page_token": null,
"previous_page_token": null,
"next_page_token": null
}
}
GET https://api.epsilon3.io/v1/builds/usage/types
Retrieve the usage types the workspace can record, ordered by name: the built-in types and the workspace's own. When authenticating with a user API key, it requires the builds:view (Builds View) permission.
Parameters
include-archived(query parameter): optional boolean. Set totrueto include archived usage types. Defaults tofalse.id(query parameter): optional usage type ID. Repeat the parameter to return several types.
Paging Parameters
limit(query parameter): optional maximum number of usage types to return per page. Must be an integer between1and1000. Defaults to1000.page-token(query parameter): optional continuation token. To fetch the next page, pass back thenext_page_tokenfrom the previous response'spaginationblock, or itsprevious_page_tokento go back a page. Use the token only as returned; do not construct or parse it. Sending a token with a differentlimitor filters returns400, so restart from the first page when they change.
The response's pagination block has limit, sort_by, sort_order, group_by, current_page_token, previous_page_token and next_page_token, which is null on the last page.
Response
- Status:
200 OK - Body:
dataholds the usage types. Every usage type has:id,name, andkind(accumulate,snapshotorcategorical).team_id: the workspace's ID. Absent on the built-in types.archived_at: when the type was archived. Absent on an active type.accumulateandsnapshottypes also haveallow_negativeandallow_reset, andunitsandprecision(the number of decimal places a reading may have) when they are set.categoricaltypes haveoptionsinstead: every option in order, archived ones included, each withid,label,positionand, when archived,archived_at. Archived options stay in the list because readings already taken still point at them.
Create Usage Types
Sample Request
import requests
import os
data = {
"usage_types": [
{
"name": "Kilometers",
"kind": "accumulate",
"units": "km",
"precision": 1,
"allow_reset": True
},
{
"name": "Oil Color",
"kind": "categorical",
"options": ["Clear", "Amber", "Dark Amber"]
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/usage/types',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/usage/types \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"usage_types": [
{
"name": "Kilometers",
"kind": "accumulate",
"units": "km",
"precision": 1,
"allow_reset": true
},
{
"name": "Oil Color",
"kind": "categorical",
"options": ["Clear", "Amber", "Dark Amber"]
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/usage/types"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
usage_types = @(
@{
name = "Kilometers"
kind = "accumulate"
units = "km"
precision = 1
allow_reset = $true
},
@{
name = "Oil Color"
kind = "categorical"
options = @("Clear", "Amber", "Dark Amber")
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"usage_types": [
{
"name": "Kilometers",
"kind": "accumulate",
"units": "km",
"precision": 1,
"allow_reset": true
},
{
"name": "Oil Color",
"kind": "categorical",
"options": ["Clear", "Amber", "Dark Amber"]
}
]
}
Example Response Body
{
"data": [
{
"id": 8,
"name": "Kilometers",
"team_id": "k8f2n5q9w3x7z1c4v6b8m2p5r7t9y3h6",
"kind": "accumulate",
"units": "km",
"precision": 1,
"allow_negative": false,
"allow_reset": true
},
{
"id": 9,
"name": "Oil Color",
"team_id": "k8f2n5q9w3x7z1c4v6b8m2p5r7t9y3h6",
"kind": "categorical",
"options": [
{ "id": 6, "label": "Clear", "position": 0 },
{ "id": 7, "label": "Amber", "position": 1 },
{ "id": 8, "label": "Dark Amber", "position": 2 }
]
}
],
"pagination": {
"limit": 2,
"sort_by": null,
"sort_order": null,
"group_by": null,
"current_page_token": null,
"previous_page_token": null,
"next_page_token": null
}
}
POST https://api.epsilon3.io/v1/builds/usage/types
Create one or more usage types. The types are created together: if any of them is rejected, none are created. When authenticating with a user API key, it requires the builds:operate (Builds Operate) permission.
Request Body
usage_types: (required) Array of 1 to 100 usage types. Each contains:name: (required) String of up to 255 characters. Names are unique within the workspace, ignoring case, and cannot match a built-in type. It cannot be changed later.kind: (required) One ofaccumulate,snapshotorcategorical. It cannot be changed later.units: (optional) String of up to 64 characters, such askm. Not allowed on acategoricaltype.precision: (optional) Number of decimal places a reading may have, from0to4. Without it, readings may have up to 4. It cannot be changed later. Not allowed on acategoricaltype.allow_negative: (optional) Whether readings may be negative. Defaults tofalse. It cannot be changed later. Cannot betrueon acategoricaltype.allow_reset: (optional) Whether the type's counters can be reset with Reset Usage Counter. Defaults tofalse. Cannot betrueon acategoricaltype.options: (required forcategorical, otherwise not allowed) Array of 2 to 500 option labels, each a string of up to 255 characters. Labels cannot be blank or repeat, ignoring case. The options keep the order they are sent in.
Response
- Status:
201 Created - Body: the created usage types in
data, in the same shape as List Usage Types, with apaginationblock that has no further pages.
A name that is already taken returns 409 (Usage type names already in use: …). A field that does not fit the type's kind, or invalid options, returns 422.
Update Usage Type
Sample Request
import requests
import os
data = {
"options": {
"added": [{"label": "Milky"}],
"updated": [
{"id": 7, "label": "Light Amber"},
{"id": 8, "position": 0},
{"id": 6, "archived": True}
]
}
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/usage/types/9',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/usage/types/9 \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"options": {
"added": [{ "label": "Milky" }],
"updated": [
{ "id": 7, "label": "Light Amber" },
{ "id": 8, "position": 0 },
{ "id": 6, "archived": true }
]
}
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/usage/types/9"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
options = @{
added = @(@{ label = "Milky" })
updated = @(
@{ id = 7; label = "Light Amber" },
@{ id = 8; position = 0 },
@{ id = 6; archived = $true }
)
}
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"options": {
"added": [{ "label": "Milky" }],
"updated": [
{ "id": 7, "label": "Light Amber" },
{ "id": 8, "position": 0 },
{ "id": 6, "archived": true }
]
}
}
Example Response Body
{
"usage_type": {
"id": 9,
"name": "Oil Color",
"team_id": "k8f2n5q9w3x7z1c4v6b8m2p5r7t9y3h6",
"kind": "categorical",
"options": [
{ "id": 8, "label": "Dark Amber", "position": 0 },
{ "id": 6, "label": "Clear", "position": 1, "archived_at": "2026-11-03T15:42:10.118Z" },
{ "id": 7, "label": "Light Amber", "position": 2 },
{ "id": 11, "label": "Milky", "position": 3 }
]
}
}
PATCH https://api.epsilon3.io/v1/builds/usage/types/:usageTypeId
Update one of the workspace's usage types. A usage type cannot be renamed, and its kind, precision and allow_negative cannot be changed, since readings already taken were checked against them. When authenticating with a user API key, it requires the builds:operate (Builds Operate) permission.
Parameters
usageTypeId(path parameter): ID of the usage type.
Request Body
Every field is optional.
name: Usage types cannot be renamed. A name other than the current one returns422.units: String of up to 64 characters, ornullto clear. Not allowed on acategoricaltype.allow_reset: Whether the type's counters can be reset. Cannot betrueon acategoricaltype.archived:truearchives the type andfalserestores it. Subjects that already declare an archived type keep it, and its readings are kept, but it cannot be newly declared, recorded against, reset, or used by a new usage trigger.options: Changes to acategoricaltype's options. Not allowed on other kinds. An object with:added: Array of 1 to 500{ "label" }objects. New options go to the end of the list.updated: Array of 1 to 500 objects, each with theidof an existing option and any of:label: new label.position: the zero-based place to move the option to.archived:truearchives the option andfalserestores it. Options are archived, never deleted, so readings that used them keep their label.
- At least 2 options must stay active, and a type holds at most 500 options, archived ones included. Labels cannot be blank or repeat, ignoring case, including archived options.
Response
- Status:
200 OK - Body:
usage_type, the updated type in the same shape as List Usage Types.
An unknown usageTypeId returns 400 (Invalid usage type: …), as does an option ID that does not belong to the type. A built-in type returns 403. A new name, a field that does not fit the type's kind, or option changes that break the rules above return 422.
List Usage Summaries
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/usage/summaries',
params={'item-id': ['Rk3vYpQ8mTzNw2cXbHdLqe'], 'tool-instance-id': [42]},
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
"https://api.epsilon3.io/v1/builds/usage/summaries?item-id=Rk3vYpQ8mTzNw2cXbHdLqe&tool-instance-id=42" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/usage/summaries?item-id=Rk3vYpQ8mTzNw2cXbHdLqe&tool-instance-id=42"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
Example Response Body
{
"summaries": [
{
"item_id": "Rk3vYpQ8mTzNw2cXbHdLqe",
"usage_type_id": 8,
"usage_type_name": "Kilometers",
"usage_type_kind": "accumulate",
"usage_type_units": "km",
"usage_type_precision": 1,
"latest_recorded_at": "2026-10-27T19:33:03.370Z",
"reading_count": 6,
"latest_value": 1.3,
"total_value": 1.3,
"reading_count_since_reset": 1,
"last_reset_at": "2026-10-21T00:59:28.954Z"
},
{
"item_id": "Rk3vYpQ8mTzNw2cXbHdLqe",
"usage_type_id": 9,
"usage_type_name": "Oil Color",
"usage_type_kind": "categorical",
"latest_recorded_at": "2026-10-24T00:23:48.108Z",
"reading_count": 3,
"latest_categorical_option_id": 7,
"latest_categorical_option_label": "Amber"
},
{
"tool_instance_id": 42,
"usage_type_id": 7,
"usage_type_name": "Max Altitude",
"usage_type_kind": "snapshot",
"usage_type_units": "ft",
"usage_type_precision": 0,
"latest_recorded_at": "2026-10-21T00:17:15.771Z",
"reading_count": 3,
"latest_value": 63,
"min_value": 43,
"max_value": 63,
"reading_count_since_reset": 3
}
]
}
GET https://api.epsilon3.io/v1/builds/usage/summaries
Retrieve the current usage of one or more subjects: one summary for each subject and usage type that has readings. Totals count from the latest reset. For the individual readings behind a summary, use List Usage Readings. When authenticating with a user API key, it requires the builds:view (Builds View) permission.
Parameters
item-id(query parameter): optional inventory item ID. Repeat the parameter for up to 50 items. An item in a project you cannot view returns403.tool-instance-id(query parameter): optional tool instance ID. Repeat the parameter for up to 50 tool instances.location-id(query parameter): optional location ID. Repeat the parameter for up to 50 locations.
Pass at least one subject; without one the list is empty. This endpoint is not paginated.
Response
- Status:
200 OK - Body:
summaries, an array in which every summary has:- Exactly one of
tool_instance_id,item_idorlocation_id. usage_type_id,usage_type_name,usage_type_kind, andusage_type_unitswhen the type has units.latest_recorded_atandreading_count, counting every reading, including those before a reset.- For
accumulatetypes:total_value, the total since the latest reset;reading_count_since_reset;latest_value, absent when the latest reading is a reset;last_reset_at, absent if the counter has never been reset; andusage_type_precisionwhen set. - For
snapshottypes:latest_value, absent after a reset;min_valueandmax_valuesince the latest reset;reading_count_since_reset;last_reset_at; andusage_type_precisionwhen set. - For
categoricaltypes:latest_categorical_option_idandlatest_categorical_option_label.
- Exactly one of
For example, an item whose Kilometers readings are 12, 8 and 5, then a reset, then 3 has a summary with total_value 3, latest_value 3, reading_count 5 (the reset counts as a reading) and reading_count_since_reset 1.
List Usage Readings
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/usage/readings',
params={'item-id': 'Rk3vYpQ8mTzNw2cXbHdLqe'},
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
"https://api.epsilon3.io/v1/builds/usage/readings?item-id=Rk3vYpQ8mTzNw2cXbHdLqe" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/usage/readings?item-id=Rk3vYpQ8mTzNw2cXbHdLqe"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
Example Response Body
{
"data": [
{
"id": "626e2d7c-d364-4539-8894-faf6e5619dcd",
"usage_type_id": 8,
"item_id": "Rk3vYpQ8mTzNw2cXbHdLqe",
"recorded_at": "2026-10-27T19:33:03.370Z",
"recorded_by": "YfsK2SFZ8afndGo2u79ruB",
"value": 1.3
},
{
"id": "a027f6c2-4c54-4c1b-84ee-aa707cdd11e1",
"usage_type_id": 9,
"item_id": "Rk3vYpQ8mTzNw2cXbHdLqe",
"recorded_at": "2026-10-24T00:23:48.108Z",
"recorded_by": "YfsK2SFZ8afndGo2u79ruB",
"content_id": "ct_mlAMjcYEL0r14GIYW2as",
"run": {
"id": "b3f1c9d2-6a47-4e0c-8d15-52a7f3e9c604",
"code": "R-0017",
"name": "Propulsion Assembly",
"run_number": 17,
"step": { "id": "step_sgt8ko2ny6", "name": "Oil Check", "number": "B1" }
},
"categorical_option_id": 7,
"categorical_option_label": "Amber"
},
{
"id": "91e7512b-3c1a-4804-905f-441c894d62f7",
"usage_type_id": 8,
"item_id": "Rk3vYpQ8mTzNw2cXbHdLqe",
"recorded_at": "2026-10-21T00:59:28.954Z",
"recorded_by": "YfsK2SFZ8afndGo2u79ruB",
"is_reset": true
}
],
"pagination": {
"limit": 1000,
"sort_by": null,
"sort_order": null,
"group_by": null,
"current_page_token": null,
"previous_page_token": null,
"next_page_token": null
}
}
GET https://api.epsilon3.io/v1/builds/usage/readings
Retrieve usage readings, newest first, including readings recorded in runs and resets. For a subject's current total, use List Usage Summaries instead. When authenticating with a user API key, it requires the builds:view (Builds View) permission.
Parameters
Each filter takes one value. Filters are combined with AND.
usage-type-id(query parameter): optional usage type ID.tool-instance-id(query parameter): optional tool instance ID.item-id(query parameter): optional inventory item ID. An item in a project you cannot view returns403.location-id(query parameter): optional location ID.run-id(query parameter): optional run ID, for readings recorded in that run.
Paging Parameters
limit(query parameter): optional maximum number of readings to return per page. Must be an integer between1and1000. Defaults to1000.page-token(query parameter): optional continuation token. To fetch the next page, pass back thenext_page_tokenfrom the previous response'spaginationblock, or itsprevious_page_tokento go back a page. Use the token only as returned; do not construct or parse it. Sending a token with a differentlimitor filters returns400, so restart from the first page when they change.
The response's pagination block has limit, sort_by, sort_order, group_by, current_page_token, previous_page_token and next_page_token, which is null on the last page.
Response
- Status:
200 OK - Body:
dataholds the readings. Every reading has:id,usage_type_idandrecorded_at.- Exactly one of
tool_instance_id,item_idorlocation_id: the subject the reading was recorded against. recorded_by: the ID of the user who recorded it, including through a user API key or by signing off a run step. Absent when no user on the workspace recorded it, such as with a workspace API key or by an automation.- Exactly one of:
value: the number recorded, foraccumulateandsnapshottypes. For anaccumulatetype it is the amount added, even when the reading was sent as a current reading.categorical_option_idandcategorical_option_label: the option recorded, forcategoricaltypes.is_reset:truewhen the reading is a reset made with Reset Usage Counter.
runandcontent_id: present when the reading was recorded in a run.runhas the run'sid,code,name,run_numberand thestep(id,name,number). When you cannot view the run's project,runis reduced to itsidand"restricted": true.
Create Usage Reading
Sample Request
import requests
import os
data = {
"usage_type_id": 8,
"tool_instance_id": 42,
"value": 12.5
}
requests.post(
url='https://api.epsilon3.io/v1/builds/usage/readings',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/usage/readings \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"usage_type_id": 8,
"tool_instance_id": 42,
"value": 12.5
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/usage/readings"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
usage_type_id = 8
tool_instance_id = 42
value = 12.5
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body (Amount Added)
{
"usage_type_id": 8,
"tool_instance_id": 42,
"value": 12.5
}
Example Request Body (Current Meter Reading)
{
"usage_type_id": 8,
"tool_instance_id": 42,
"value": 1520.4,
"entry_mode": "current_reading"
}
Example Request Body (Category)
{
"usage_type_id": 9,
"item_id": "Rk3vYpQ8mTzNw2cXbHdLqe",
"categorical_option_id": 7
}
Example Response Body
{
"reading": {
"id": "5c0e8a7d-91f2-4b3e-a6d4-2f7b9c1e8a30",
"usage_type_id": 8,
"tool_instance_id": 42,
"recorded_at": "2026-11-03T15:42:10.118Z",
"value": 12.5
}
}
POST https://api.epsilon3.io/v1/builds/usage/readings
Record one reading, outside a run, against a tool instance, an inventory item or a location. The subject's maintenance statuses are recomputed once the reading is saved. When authenticating with a user API key, it requires the builds:operate (Builds Operate) permission, and recording against an item also requires access to the item's project.
Request Body
usage_type_id: (required) ID of the usage type. The subject must declare it: an item through its part revision, a tool instance through its tool, and a location directly.tool_instance_id,item_id,location_id: (required) exactly one of these, naming the subject.value: (required foraccumulateandsnapshottypes) The number recorded. It may have at most the type'sprecisionin decimal places, or 4 when the type sets none.categorical_option_id: (required forcategoricaltypes) ID of one of the type's active options.entry_mode: (optional) How anaccumulatevalue is read. Ignored for other kinds. Must be one of:delta: the amount used since the last reading. This is the default.current_reading: the meter's current total. The reading is stored as the difference from the subject's current total.
A negative value needs a type with allow_negative, with one exception: an accumulate amount added may be negative, to correct an earlier reading, as long as the total does not drop below 0.
Response
- Status:
201 Created - Body:
reading, the recorded reading in the same shape as List Usage Readings.
Naming no subject or several returns 400, as does an unknown or archived usage type. An item in a project where you lack builds:operate returns 403. A current_reading that races another reading on the same subject and usage type returns 409; send it again. An unknown subject, a type the subject does not declare (Usage type 8 is not declared on the tool), a value that breaks the type's rules, or a total that would drop below 0 returns 422.
Reset Usage Counter
Sample Request
import requests
import os
data = {
"usage_type_id": 8,
"tool_instance_id": 42
}
requests.post(
url='https://api.epsilon3.io/v1/builds/usage/readings/reset',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/usage/readings/reset \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"usage_type_id": 8,
"tool_instance_id": 42
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/usage/readings/reset"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
usage_type_id = 8
tool_instance_id = 42
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"usage_type_id": 8,
"tool_instance_id": 42
}
POST https://api.epsilon3.io/v1/builds/usage/readings/reset
Reset one subject's counter for one usage type. An accumulate total counts from zero again, and a snapshot stream is cleared, so it has no latest value until the next reading. A categorical type records an option rather than a number, so it has no counter and cannot be reset. The reset is kept as a reading with "is_reset": true, and the subject's maintenance statuses are recomputed. When authenticating with a user API key, it requires the builds:operate (Builds Operate) permission, and resetting an item's counter also requires access to the item's project.
Request Body
usage_type_id: (required) ID of the usage type. It must allow resets (allow_reset).tool_instance_id,item_id,location_id: (required) exactly one of these, naming the subject.
Response
- Status:
204 No Content - Body: (empty)
The response is the same whether or not a reset was written: resetting a counter that is already at its start changes nothing.
Naming no subject or several returns 400, as does an unknown or archived usage type. An item in a project where you lack builds:operate returns 403. A categorical type, or a type that does not allow resets, returns 422.
Maintenance
A maintenance routine says when a tool, part or location needs maintenance and what work to do. Each routine has one or more triggers, which set when it comes due, and bindings, which apply it to the subjects it maintains.
An unsupported query parameter or top-level body field returns 422.
Triggers
schedule: comes due on a calendar recurrence.check_outs: comes due after a number of tool check-outs.usage: comes due on a usage type's readings, such as every 500 hours run or when a pressure reads too high. Every subject the routine is bound to must declare the usage type. See Usage.
A trigger can name a procedure that carries out its maintenance. The trigger is then recorded as executed by a run of that procedure, not by hand. Triggers are kept in priority order, the first being the top. Completing a trigger also resets every trigger below it on the same subject. A check_outs, usage or last_maintained schedule trigger restarts from the completion. A fixed schedule trigger keeps its calendar: the reset only uses up its current occurrence when that occurrence is already due soon, due or overdue, or its target date falls within reset_window_days of the completion. See Create Maintenance Routines.
Bindings, statuses and events
A routine can be bound to tools, tool instances, parts, inventory items and locations. A tool binding covers each of the tool's instances, and a part binding covers each of the part's items, so maintenance is always tracked for one tool instance, item or location:
- A status is a trigger's current occurrence on one of these subjects: whether it is due and when. There is one status for each binding, trigger and subject covered. See List Maintenance Statuses.
- An event records what happened to an occurrence: it was carried out, deferred or canceled. See List Maintenance Events.
List Maintenance Routines
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/maintenance/routines',
params={'tool-id': [12], 'sort-by': 'name'},
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
"https://api.epsilon3.io/v1/builds/maintenance/routines?tool-id=12&sort-by=name" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/maintenance/routines?tool-id=12&sort-by=name"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
Example Response Body
{
"data": [
{
"id": "1a24067f-67d1-40ff-a301-c0269a825cc1",
"name": "Torque Wrench Calibration",
"reset_window_days": 7,
"triggers": [
{
"id": "464c52ac-f292-4b8b-8392-1a4b428fcff7",
"name": "Annual calibration",
"position": 1,
"procedure_id": "93yUF05qKPvzQj9VcXtRuI",
"recurrence_display_text": "Every year",
"trigger_type": "schedule",
"trigger_config": {
"start_at": "2026-01-01T00:00:00.000Z",
"rrule": "FREQ=YEARLY;INTERVAL=1",
"basis": "fixed",
"day_count": "calendar",
"offsets": { "due_soon": 30, "due": 0, "overdue": 14 }
}
},
{
"id": "694a26b8-5f6e-4b65-9481-2b5f01bd88ac",
"name": "Every 500 check-outs",
"position": 2,
"recurrence_display_text": "Every 500 check-outs",
"trigger_type": "check_outs",
"trigger_config": {
"accrued": { "due_soon": 450, "due": 500, "overdue": 550 },
"count_from": "added"
}
}
],
"should_block_use_when_due": true,
"created_at": "2026-10-15T20:09:59.051Z",
"updated_at": "2026-10-15T22:59:27.350Z",
"binding_count": 2,
"due_state_counts": {
"not_due": 5,
"due_soon": 1,
"due": 2,
"overdue": 0
}
}
],
"pagination": {
"limit": 1000,
"sort_by": "name",
"sort_order": "asc",
"group_by": null,
"current_page_token": null,
"previous_page_token": null,
"next_page_token": null
}
}
GET https://api.epsilon3.io/v1/builds/maintenance/routines
Retrieve maintenance routines with a count of their statuses by due state. When authenticating with a user API key, it requires the builds:view (Builds View) permission.
Parameters
Parameters marked as repeatable match any of their values. Different parameters are combined with AND.
q(query parameter): optional text to match against routine names.trigger-type(query parameter): optional, repeatable. Routines with a trigger of this type:schedule,usageorcheck_outs.usage-type-id(query parameter): optional, repeatable. Routines with a usage trigger on this usage type.archived-status(query parameter): optional, repeatable.activeorarchived. Defaults toactive.tool-id(query parameter): optional, repeatable. Routines bound to this tool.part-id(query parameter): optional. Routines bound to this part.tool-instance-id(query parameter): optional, repeatable. Routines bound to this tool instance or to its tool.item-id(query parameter): optional. Routines bound to this inventory item or to its part that have ascheduleorcheck_outstrigger, or ausagetrigger on a usage type the item's part revision declares.location-id(query parameter): optional, repeatable. Routines bound to this location.declared-usage-type-id(query parameter): optional, repeatable. Routines with ascheduleorcheck_outstrigger, or ausagetrigger on one of these usage types.
Paging Parameters
limit(query parameter): optional maximum number of routines to return per page. Must be an integer between1and1000. Defaults to1000.page-token(query parameter): optional continuation token. To fetch the next page, pass back thenext_page_tokenfrom the previous response'spaginationblock, or itsprevious_page_tokento go back a page. Use the token only as returned; do not construct or parse it. Sending a token with a differentlimit, sort or filters returns400, so restart from the first page when they change.sort-by(query parameter): optional.name, ortrigger-typefor the type of the routine's top trigger. Without it, routines are ordered by ID.sort-order(query parameter): optional.asc(the default) ordesc.
The response's pagination block has limit, sort_by, sort_order, group_by, current_page_token, previous_page_token and next_page_token, which is null on the last page.
Response
- Status:
200 OK - Body:
dataholds the routines. Every routine has:id,name,created_atandupdated_at.reset_window_daysandreset_window_units, when set.should_block_use_when_due: whether the routine's subjects are blocked from being picked for use while it is due or overdue.archived_at: when the routine was archived. Absent on an active routine.triggers: in priority order. Every trigger hasid,name,position(1 being the top),trigger_type,trigger_config(see Trigger Configuration), andrecurrence_display_text, the trigger's cadence in words. A trigger also hasprocedure_idwhen it names a procedure, and a usage trigger hasusage_type_idand itsusage_type.binding_count: the number of subjects the routine is bound to.due_state_counts: the number of subjects the routine covers in each due state:not_due,due_soon,dueandoverdue. A subject with several triggers is counted once, at its most urgent trigger's state.
Create Maintenance Routines
Sample Request
import requests
import os
data = {
"routines": [
{
"name": "Torque Wrench Calibration",
"should_block_use_when_due": True,
"reset_window_days": 7,
"triggers": [
{
"name": "Annual calibration",
"trigger_type": "schedule",
"procedure_id": "93yUF05qKPvzQj9VcXtRuI",
"trigger_config": {
"start_at": "2026-01-01T00:00:00.000Z",
"rrule": "FREQ=YEARLY;INTERVAL=1",
"basis": "fixed",
"day_count": "calendar",
"offsets": {"due_soon": 30, "due": 0, "overdue": 14}
}
},
{
"name": "Every 500 check-outs",
"trigger_type": "check_outs",
"trigger_config": {
"accrued": {"due_soon": 450, "due": 500, "overdue": 550}
}
}
],
"bindings": [
{"tool_id": 12},
{"tool_instance_id": 57}
]
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/maintenance/routines',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/maintenance/routines \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"routines": [
{
"name": "Torque Wrench Calibration",
"should_block_use_when_due": true,
"reset_window_days": 7,
"triggers": [
{
"name": "Annual calibration",
"trigger_type": "schedule",
"procedure_id": "93yUF05qKPvzQj9VcXtRuI",
"trigger_config": {
"start_at": "2026-01-01T00:00:00.000Z",
"rrule": "FREQ=YEARLY;INTERVAL=1",
"basis": "fixed",
"day_count": "calendar",
"offsets": { "due_soon": 30, "due": 0, "overdue": 14 }
}
},
{
"name": "Every 500 check-outs",
"trigger_type": "check_outs",
"trigger_config": {
"accrued": { "due_soon": 450, "due": 500, "overdue": 550 }
}
}
],
"bindings": [
{ "tool_id": 12 },
{ "tool_instance_id": 57 }
]
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/maintenance/routines"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
routines = @(
@{
name = "Torque Wrench Calibration"
should_block_use_when_due = $true
reset_window_days = 7
triggers = @(
@{
name = "Annual calibration"
trigger_type = "schedule"
procedure_id = "93yUF05qKPvzQj9VcXtRuI"
trigger_config = @{
start_at = "2026-01-01T00:00:00.000Z"
rrule = "FREQ=YEARLY;INTERVAL=1"
basis = "fixed"
day_count = "calendar"
offsets = @{ due_soon = 30; due = 0; overdue = 14 }
}
},
@{
name = "Every 500 check-outs"
trigger_type = "check_outs"
trigger_config = @{
accrued = @{ due_soon = 450; due = 500; overdue = 550 }
}
}
)
bindings = @(
@{ tool_id = 12 },
@{ tool_instance_id = 57 }
)
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"routines": [
{
"name": "Torque Wrench Calibration",
"should_block_use_when_due": true,
"reset_window_days": 7,
"triggers": [
{
"name": "Annual calibration",
"trigger_type": "schedule",
"procedure_id": "93yUF05qKPvzQj9VcXtRuI",
"trigger_config": {
"start_at": "2026-01-01T00:00:00.000Z",
"rrule": "FREQ=YEARLY;INTERVAL=1",
"basis": "fixed",
"day_count": "calendar",
"offsets": { "due_soon": 30, "due": 0, "overdue": 14 }
}
},
{
"name": "Every 500 check-outs",
"trigger_type": "check_outs",
"trigger_config": {
"accrued": { "due_soon": 450, "due": 500, "overdue": 550 }
}
}
],
"bindings": [
{ "tool_id": 12 },
{ "tool_instance_id": 57 }
]
}
]
}
Example Response Body
{
"routines": [
{
"id": "1a24067f-67d1-40ff-a301-c0269a825cc1",
"name": "Torque Wrench Calibration",
"reset_window_days": 7,
"triggers": [
{
"id": "464c52ac-f292-4b8b-8392-1a4b428fcff7",
"name": "Annual calibration",
"position": 1,
"procedure_id": "93yUF05qKPvzQj9VcXtRuI",
"recurrence_display_text": "Every year",
"trigger_type": "schedule",
"trigger_config": {
"start_at": "2026-01-01T00:00:00.000Z",
"rrule": "FREQ=YEARLY;INTERVAL=1",
"basis": "fixed",
"day_count": "calendar",
"offsets": { "due_soon": 30, "due": 0, "overdue": 14 }
}
},
{
"id": "694a26b8-5f6e-4b65-9481-2b5f01bd88ac",
"name": "Every 500 check-outs",
"position": 2,
"recurrence_display_text": "Every 500 check-outs",
"trigger_type": "check_outs",
"trigger_config": {
"accrued": { "due_soon": 450, "due": 500, "overdue": 550 },
"count_from": "added"
}
}
],
"should_block_use_when_due": true,
"created_at": "2026-11-03T15:42:10.118Z",
"updated_at": "2026-11-03T15:42:10.118Z",
"binding_count": 2,
"due_state_counts": {
"not_due": 0,
"due_soon": 0,
"due": 0,
"overdue": 0
}
}
]
}
POST https://api.epsilon3.io/v1/builds/maintenance/routines
Create one or more maintenance routines. The routines are created together: if any of them is rejected, none are created. Statuses for the new routines are worked out in the background, so due_state_counts and List Maintenance Statuses can take a moment to fill in. When authenticating with a user API key, it requires the builds:edit (Builds Edit) permission, and binding a part or an item, or naming a procedure, also requires access to its project.
Request Body
routines: (required) Array of 1 to 100 routines. Each contains:name: (required) String of up to 255 characters.triggers: (required) Array of 1 to 20 triggers, in priority order: the first is the top. Each contains:name: (required) String of up to 255 characters.trigger_type: (required)schedule,usageorcheck_outs. It cannot be changed later.usage_type_id: (required forusagetriggers, otherwise not allowed) ID of an active usage type. It cannot be changed later.trigger_config: (required) When the trigger comes due. Its shape depends on the trigger type and, for a usage trigger, on the usage type's kind. See Trigger Configuration.procedure_id: (optional) ID of a released, unarchived procedure that carries out the maintenance. An execution of the trigger then has to come from a successful run of that procedure; see Create Maintenance Events.
bindings: (optional) Array of 0 to 100 subjects to apply the routine to. Each names exactly one oftool_id,tool_instance_id,part_id,item_idorlocation_id.should_block_use_when_due: (optional) Whether the routine's subjects are blocked from being picked for use while it is due or overdue. Defaults tofalse.reset_window_days: (optional) Whole number of days,0or more. When a higher trigger is completed, a lowerfixedscheduletrigger's occurrence is used up if its target date falls within this many days of the completion. It also sets when a status'sreset_display_textnames a higherscheduletrigger.reset_window_units: (optional) Number,0or more. It only sets when a status'sreset_display_textnames a highercheck_outstrigger, or a higheraccumulateusage trigger on the same usage type.
Completing a trigger resets every trigger below it, whatever the windows. A trigger reset by a higher one gets an event of its own, with covered_by naming the trigger that reset it.
Binding rules
- A
check_outstrigger cannot apply to a location. - When the routine has usage triggers, every subject must declare every usage type they use:
- A tool through its own declarations, and a tool instance through its tool's.
- An item through its part revision's.
- A part when at least one of its revisions declares all of them. Types split across revisions do not count.
- A location through its own.
- A routine cannot be bound to a tool and to one of that tool's instances, or to a part and to one of that part's items.
- Scrapped, sold, shipped and rejected items cannot be bound, nor can archived tool instances.
Response
- Status:
201 Created - Body:
routines, the created routines in the same shape as List Maintenance Routines.
Errors
400: a subject that does not exist (Invalid binding subjects: …), a scrapped, sold, shipped or rejected item, or an unknown usage type.403: a part, item or procedure in a project where you lackbuilds:edit.404: an unknown, unreleased or archived procedure.422: a binding that names no subject or several, overlapping bindings, an archived tool instance, a usage trigger without a usage type or with an archived one, or a trigger configuration or binding that breaks the rules above.
Trigger Configuration
trigger_config sets when a trigger comes due. Each trigger moves through the due states not_due, due_soon, due and overdue, and its thresholds set where each state begins. due is always required. A threshold left out turns that state off: without due_soon, the trigger goes straight from not_due to due. Thresholds have to be in order, so due_soon comes before due and due before overdue. Any other key returns 422.
Example Trigger Configuration (Schedule)
{
"start_at": "2026-01-01T09:00:00.000-05:00",
"rrule": "FREQ=MONTHLY;INTERVAL=3",
"basis": "last_maintained",
"day_count": "working",
"offsets": { "due_soon": 10, "due": 0, "overdue": 5 }
}
schedule: every key is required.
start_at: ISO 8601 timestamp with an explicit offset, such asZor-05:00. The first target date.rrule: RFC 5545 recurrence rule of up to 1024 characters, such asFREQ=MONTHLY;INTERVAL=3.FREQis required and must beDAILY,WEEKLY,MONTHLYorYEARLY.INTERVALandCOUNTmust be at least 1,UNTILmust be afterstart_at, andCOUNTandUNTILcannot both be set.DTSTARTis not allowed; usestart_at.basis: what the next target date counts from:fixed: the recurrence itself, regardless of when the work is done.last_maintained: the last time the work was done. With this basis the rule cannot useBYDAY,BYMONTHDAY,BYMONTH,BYSETPOS,BYYEARDAYorBYWEEKNO.
day_count: whetheroffsetscountcalendardays orworkingdays.offsets: days around each target date.due_soonis how many days before the target the trigger is due soon, from 1 to 365.dueis how many days before the target it is due, from 0 (on the target date) to 365, and must be smaller thandue_soon.overdueis how many days after the target it is overdue, from 1 to 365.
For example, with a target date of June 15, calendar days and offsets { "due_soon": 10, "due": 0, "overdue": 5 }, the trigger is due soon from June 5, due from June 15 and overdue from June 20.
Example Trigger Configuration (Check-outs)
{
"accrued": { "due_soon": 450, "due": 500, "overdue": 550 },
"count_from": "added"
}
check_outs:
accrued: (required) Check-outs since the last maintenance at which each state begins. Whole numbers of 1 or more, increasing fromdue_soontooverdue.count_from: (optional) Where the count starts before the first maintenance:added, when the routine was applied to the subject (the default), orall_history, the subject's whole history. It cannot be changed later.
Example Trigger Configuration (Usage, Accumulate)
{
"accrued": { "due_soon": 450, "due": 500, "overdue": 520.5 },
"count_from": "all_history"
}
usage on an accumulate type: the same keys as check_outs, counting the usage type's total since the last maintenance. accrued thresholds are numbers greater than 0 and do not have to be whole.
Example Trigger Configuration (Usage, Snapshot)
{
"low": { "due": 20, "overdue": 10 },
"high": { "due_soon": 90, "due": 100 }
}
usage on a snapshot type: thresholds on the latest reading.
high: (optional) Each state begins when the latest reading is at or above its threshold. Thresholds increase fromdue_soontooverdue.low: (optional) Each state begins when the latest reading is at or below its threshold. Thresholds decrease fromdue_soontooverdue.
At least one of high and low must be set, and every low threshold must be below every high threshold. Thresholds cannot be negative unless the usage type allows negative readings.
Example Trigger Configuration (Usage, Categorical)
{
"options": { "due_soon": [7], "due": [8, 9] }
}
usage on a categorical type: the options that put the trigger in each state.
options: (required) Object withdue, a list of at least one option ID, and optionaldue_soonandoverduelists. Each must be an active option of the usage type. An option can be listed only once across all three lists.
Get Maintenance Routine
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/maintenance/routines/1a24067f-67d1-40ff-a301-c0269a825cc1',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/maintenance/routines/1a24067f-67d1-40ff-a301-c0269a825cc1 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/maintenance/routines/1a24067f-67d1-40ff-a301-c0269a825cc1"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
Example Response Body
{
"routine": {
"id": "1a24067f-67d1-40ff-a301-c0269a825cc1",
"name": "Torque Wrench Calibration",
"reset_window_days": 7,
"triggers": [
{
"id": "464c52ac-f292-4b8b-8392-1a4b428fcff7",
"name": "Annual calibration",
"position": 1,
"procedure_id": "93yUF05qKPvzQj9VcXtRuI",
"recurrence_display_text": "Every year",
"trigger_type": "schedule",
"trigger_config": {
"start_at": "2026-01-01T00:00:00.000Z",
"rrule": "FREQ=YEARLY;INTERVAL=1",
"basis": "fixed",
"day_count": "calendar",
"offsets": { "due_soon": 30, "due": 0, "overdue": 14 }
}
},
{
"id": "694a26b8-5f6e-4b65-9481-2b5f01bd88ac",
"name": "Every 500 check-outs",
"position": 2,
"recurrence_display_text": "Every 500 check-outs",
"trigger_type": "check_outs",
"trigger_config": {
"accrued": { "due_soon": 450, "due": 500, "overdue": 550 },
"count_from": "added"
}
}
],
"should_block_use_when_due": true,
"created_at": "2026-10-15T20:09:59.051Z",
"updated_at": "2026-10-15T22:59:27.350Z",
"binding_count": 2,
"due_state_counts": {
"not_due": 5,
"due_soon": 1,
"due": 2,
"overdue": 0
},
"bindings": [
{
"id": "0d49baf2-71bc-49bb-a6ec-666b37111854",
"routine_id": "1a24067f-67d1-40ff-a301-c0269a825cc1",
"subject_type": "tool",
"tool": {
"id": 12,
"tool_number": "TL-014",
"name": "Torque Wrench"
},
"leaf_count": 7
},
{
"id": "163f9ab9-2a7c-4a25-b86a-2b49d20eedee",
"routine_id": "1a24067f-67d1-40ff-a301-c0269a825cc1",
"subject_type": "tool_instance",
"tool_instance": {
"id": 57,
"tool_id": 15,
"serial_number": "SN-2207",
"status": "available",
"location_id": 17
},
"tool": {
"id": 15,
"tool_number": "TL-022",
"name": "Digital Torque Wrench"
},
"leaf_count": 1
}
]
}
}
GET https://api.epsilon3.io/v1/builds/maintenance/routines/:routineId
Retrieve one maintenance routine with its bindings. Archived routines can be retrieved too. When authenticating with a user API key, it requires the builds:view (Builds View) permission.
Parameters
routineId(path parameter): ID of the routine.
Response
- Status:
200 OK - Body:
routine, in the same shape as List Maintenance Routines withbindingsadded. Every binding has itsid,routine_id,leaf_count(the number of tool instances, items or locations it covers), and asubject_typethat says which other fields it has:tool: atoolobject.tool_instance: atool_instanceobject and itstool.part: apartobject. When you cannot view the part's project, it is reduced to itsidand the binding has noleaf_count.item: anitemobject and itspart. When you cannot view the item's project, both are reduced to theiridand the binding has noleaf_count.location:location_idandlocation_name.
An unknown routineId returns 404.
Update Maintenance Routine
Sample Request
import requests
import os
data = {
"should_block_use_when_due": False,
"triggers": {
"updated": [
{
"id": "694a26b8-5f6e-4b65-9481-2b5f01bd88ac",
"trigger_config": {
"accrued": {"due_soon": 900, "due": 1000}
},
"position": 1
}
]
},
"bindings": {
"added": [{"part_id": "u9HfWj4sKxVn7LrEoYtB1d"}],
"removed": [{"id": "163f9ab9-2a7c-4a25-b86a-2b49d20eedee"}]
}
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/maintenance/routines/1a24067f-67d1-40ff-a301-c0269a825cc1',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/maintenance/routines/1a24067f-67d1-40ff-a301-c0269a825cc1 \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"should_block_use_when_due": false,
"triggers": {
"updated": [
{
"id": "694a26b8-5f6e-4b65-9481-2b5f01bd88ac",
"trigger_config": {
"accrued": { "due_soon": 900, "due": 1000 }
},
"position": 1
}
]
},
"bindings": {
"added": [{ "part_id": "u9HfWj4sKxVn7LrEoYtB1d" }],
"removed": [{ "id": "163f9ab9-2a7c-4a25-b86a-2b49d20eedee" }]
}
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/maintenance/routines/1a24067f-67d1-40ff-a301-c0269a825cc1"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
should_block_use_when_due = $false
triggers = @{
updated = @(
@{
id = "694a26b8-5f6e-4b65-9481-2b5f01bd88ac"
trigger_config = @{
accrued = @{ due_soon = 900; due = 1000 }
}
position = 1
}
)
}
bindings = @{
added = @(@{ part_id = "u9HfWj4sKxVn7LrEoYtB1d" })
removed = @(@{ id = "163f9ab9-2a7c-4a25-b86a-2b49d20eedee" })
}
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"should_block_use_when_due": false,
"triggers": {
"updated": [
{
"id": "694a26b8-5f6e-4b65-9481-2b5f01bd88ac",
"trigger_config": {
"accrued": { "due_soon": 900, "due": 1000 }
},
"position": 1
}
]
},
"bindings": {
"added": [{ "part_id": "u9HfWj4sKxVn7LrEoYtB1d" }],
"removed": [{ "id": "163f9ab9-2a7c-4a25-b86a-2b49d20eedee" }]
}
}
PATCH https://api.epsilon3.io/v1/builds/maintenance/routines/:routineId
Update a maintenance routine. Triggers and bindings are changed with added, updated and removed lists, so anything a request leaves out stays as it is. Statuses are worked out again in the background. Routines are never deleted; archive one instead. When authenticating with a user API key, it requires the builds:edit (Builds Edit) permission, and adding or removing a part or item binding, or naming a procedure, also requires access to its project.
Parameters
routineId(path parameter): ID of the routine.
Request Body
Every field is optional.
name: String of up to 255 characters.reset_window_days,reset_window_units: as on Create Maintenance Routines, ornullto clear.should_block_use_when_due: boolean.archived:truearchives the routine andfalserestores it. An archived routine has no statuses.triggers: Object with any of:added: Array of new triggers, as on Create Maintenance Routines. They go below the existing triggers.updated: Array of changes to existing triggers, each with the trigger'sidand any of:name: String of up to 255 characters.trigger_config: replaces the stored configuration as a whole.count_fromcannot change: leave it out to keep the stored value.procedure_id: ID of a released, unarchived procedure, ornullto clear.position: Place to move the trigger to, from 1 (the top) to 20. The other triggers are renumbered around it.- A trigger's
trigger_typeandusage_type_idcannot change. To change them, remove the trigger and add a new one.
removed: Array of{ "id" }objects. Events already recorded keep naming a removed trigger.- A routine keeps 1 to 20 triggers, and a trigger cannot be both updated and removed, or updated twice, in one request.
bindings: Object with any of:added: Array of subjects, as on Create Maintenance Routines. Adding a subject that was removed before brings back its binding, with its history.removed: Array of{ "id" }objects naming bindings to remove. A removed binding has no statuses, and its events are kept.- The binding rules apply to the routine after the change, except that a tool instance already bound before it was archived stays bound.
Response
- Status:
200 OK - Body:
routine, in the same shape as Get Maintenance Routine.
An unknown routineId returns 404. An unknown trigger ID returns 400 (Invalid maintenance routine triggers: …), as does a binding ID that is not one of the routine's (Invalid maintenance routine bindings: …). The other errors are the same as on Create Maintenance Routines.
List Maintenance Statuses
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/maintenance/statuses',
params={'due-state': ['due', 'overdue'], 'subject-type': 'item'},
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
"https://api.epsilon3.io/v1/builds/maintenance/statuses?due-state=due&due-state=overdue&subject-type=item" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/maintenance/statuses?due-state=due&due-state=overdue&subject-type=item"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
Example Response Body
{
"data": [
{
"id": "7658ffba-47f1-4fd5-9396-2fa0de30909a",
"subject": {
"subject_type": "item",
"item": {
"id": "Rk3vYpQ8mTzNw2cXbHdLqe",
"part_id": "u9HfWj4sKxVn7LrEoYtB1d",
"part_revision_id": "e5TgZc0aQmPy3NvXkJsW8h",
"tracking_id": "SN-0042",
"quantity": 1,
"status": "In Inventory"
},
"part": {
"id": "u9HfWj4sKxVn7LrEoYtB1d",
"revision_id": "e5TgZc0aQmPy3NvXkJsW8h",
"part_no": "VLV-200",
"revision": "A",
"name": "Relief Valve",
"project_id": null,
"allow_decimal_quantities": false,
"tracking": "serial",
"units": null
}
},
"binding_id": "0d49baf2-71bc-49bb-a6ec-666b37111854",
"routine_id": "c5e9d1a2-3b7f-4e60-9a18-6f2d4b8c0e71",
"routine_name": "Valve Service",
"trigger_id": "a9c917ba-a65c-4d70-b1d0-73594a4f3031",
"trigger_name": "Every 500 hours",
"trigger_type": "usage",
"procedure_id": "93yUF05qKPvzQj9VcXtRuI",
"due_state": "overdue",
"should_block_use_when_due": false,
"recurrence_display_text": "Every 500 hours",
"accrued_value": 512.5,
"accrued_threshold": 500,
"usage_type": {
"id": 10,
"name": "Run Hours",
"team_id": "k8f2n5q9w3x7z1c4v6b8m2p5r7t9y3h6",
"kind": "accumulate",
"units": "hours",
"precision": 1,
"allow_negative": false,
"allow_reset": true
},
"evaluated_at": "2026-11-02T18:20:41.502Z"
},
{
"id": "91d7601b-900e-4388-95b7-15b4c76e5c7b",
"subject": {
"subject_type": "item",
"item": {
"id": "8aeRldXdxooNhfHINSPl4J",
"part_id": "LucQYJATGBy9B3RZjOU9iY",
"part_revision_id": "ki6QgO0wXrAtPOA5bAScyV",
"tracking_id": "ETC100001",
"quantity": 1,
"status": "In Inventory"
},
"part": {
"id": "LucQYJATGBy9B3RZjOU9iY",
"revision_id": "ki6QgO0wXrAtPOA5bAScyV",
"part_no": "CAR-100010",
"revision": "1",
"name": "Drive Shaft",
"project_id": null,
"allow_decimal_quantities": false,
"tracking": "serial",
"units": null
}
},
"binding_id": "2683ff21-9e51-490b-93ab-62d6e7d0a131",
"routine_id": "8326114e-ab2a-4fd9-9cbc-fb9d6eb66e91",
"routine_name": "Drive Shaft Service",
"trigger_id": "0e820a93-ebc3-4201-a7f2-a24b6c6b5dcb",
"trigger_name": "Monthly service",
"trigger_type": "schedule",
"due_state": "due",
"next_due_at": "2026-11-01T20:09:50.000Z",
"window_opens_at": "2026-10-25T20:09:50.000Z",
"should_block_use_when_due": true,
"recurrence_display_text": "Every month after last maintenance",
"in_progress_run": {
"_id": "iqr3Zyrp1520LidfNXlGnO",
"source_run": null,
"code": "015",
"name": "Drive Shaft Service",
"run_number": 64,
"state": "running",
"status": null,
"total_steps": 15,
"completed_steps": 6,
"skipped_steps": 0,
"failed_steps": 0,
"not_required_steps": 0
},
"evaluated_at": "2026-11-02T18:20:41.502Z"
}
],
"pagination": {
"limit": 1000,
"sort_by": null,
"sort_order": null,
"group_by": null,
"current_page_token": null,
"previous_page_token": null,
"next_page_token": null
}
}
GET https://api.epsilon3.io/v1/builds/maintenance/statuses
Retrieve maintenance statuses: for each binding, trigger and tool instance, item or location the binding covers, whether the trigger's current occurrence is due. By default, the most urgent come first: overdue, then due, due soon and not due, each ordered by next due date. Archived routines, removed bindings and archived tool instances have no statuses. When authenticating with a user API key, it requires the builds:view (Builds View) permission.
Parameters
Parameters marked as repeatable match any of their values. Different parameters are combined with AND.
routine-id(query parameter): optional routine ID.binding-id(query parameter): optional binding ID.due-state(query parameter): optional, repeatable.not_due,due_soon,dueoroverdue.trigger-type(query parameter): optional, repeatable.schedule,usageorcheck_outs.subject-type(query parameter): optional, repeatable.tool_instance,itemorlocation.tool-instance-id(query parameter): optional tool instance ID.item-id(query parameter): optional inventory item ID.location-id(query parameter): optional location ID.q(query parameter): optional text to match against the routine name, a tool instance's serial number or tool number, an item's tracking ID or part number, or a location name.
Paging Parameters
limit(query parameter): optional maximum number of statuses to return per page. Must be an integer between1and1000. Defaults to1000.page-token(query parameter): optional continuation token. To fetch the next page, pass back thenext_page_tokenfrom the previous response'spaginationblock, or itsprevious_page_tokento go back a page. Use the token only as returned; do not construct or parse it. Sending a token with a differentlimit, sort or filters returns400, so restart from the first page when they change.sort-by(query parameter): optional.status, the default order, ornext-due-at.sort-order(query parameter): optional.asc(the default) ordesc.
The response's pagination block has limit, sort_by, sort_order, group_by, current_page_token, previous_page_token and next_page_token, which is null on the last page.
Response
- Status:
200 OK - Body:
dataholds the statuses. Every status has:id,binding_id,routine_id,routine_name,trigger_id,trigger_nameandtrigger_type.subject: the tool instance, item or location, read by itssubject_type:tool_instance:tool_instanceand itstool.item:itemand itspart. When you cannot view the item's project, both are reduced to theirid.location:location_idandlocation_name.
due_state:not_due,due_soon,dueoroverdue.should_block_use_when_due: the routine's setting.recurrence_display_text: the trigger's cadence in words.evaluated_at: when the status was last worked out.- These fields when they apply:
procedure_id: the trigger's procedure.next_due_at: the target date of the occurrence, when it has one.window_opens_at: when ascheduleoccurrence becomes due soon. Absent without adue_soonoffset.accrued_value: check-outs or usage since the last maintenance, or the latest reading of asnapshotusage type.accrued_threshold: the trigger'sduethreshold fromaccrued, oncheck_outsandaccumulateusage triggers.latest_categorical_option_label: the latest reading of acategoricalusage type.usage_type: the usage type of ausagetrigger.in_progress_run: the run under way on the occurrence, with its_id,source_run,code,name,run_number,state,statusand step counts. When you cannot view the run's project, it is reduced toid(not_id) and"restricted": true.work_order_id: the open work order that has planned the occurrence.reset_display_text: names the highest trigger above this one that comes due at about the same time, as a reminder that completing it will reset this one. Twoscheduletriggers line up when their target dates fall within the routine'sreset_window_days. Twocheck_outstriggers, or twoaccumulateusage triggers on the same usage type, line up when the check-outs or units left on each differ by no more than itsreset_window_units. Twosnapshotorcategoricalusage triggers on the same usage type line up when both are due. Absent when no higher trigger lines up.
List Maintenance Events
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/maintenance/events',
params={'tool-instance-id': 57, 'outcome': ['executed', 'deferred']},
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
"https://api.epsilon3.io/v1/builds/maintenance/events?tool-instance-id=57&outcome=executed&outcome=deferred" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/maintenance/events?tool-instance-id=57&outcome=executed&outcome=deferred"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
Example Response Body
{
"data": [
{
"id": "7f658e05-e5d7-4a10-be87-2ab454465f6e",
"subject": {
"subject_type": "tool_instance",
"tool_instance": {
"id": 57,
"tool_id": 15,
"serial_number": "SN-2207",
"status": "available",
"location_id": 17
},
"tool": {
"id": 15,
"tool_number": "TL-022",
"name": "Digital Torque Wrench"
}
},
"binding_id": "163f9ab9-2a7c-4a25-b86a-2b49d20eedee",
"trigger_id": "464c52ac-f292-4b8b-8392-1a4b428fcff7",
"trigger_name": "Annual calibration",
"name": "Torque Wrench Calibration",
"status": "completed",
"outcome": "executed",
"due_at": "2027-01-01T00:00:00.000Z",
"maintained_at": "2026-10-08T02:12:54.882Z",
"closed_at": "2026-10-08T02:12:54.882Z",
"run_ids": ["5pR1MTv0z4wyiBRDIl977a"],
"run": {
"_id": "5pR1MTv0z4wyiBRDIl977a",
"source_run": null,
"code": "031",
"name": "Torque Wrench Calibration",
"run_number": 12,
"state": "completed",
"status": "success",
"total_steps": 8,
"completed_steps": 8,
"skipped_steps": 0,
"failed_steps": 0,
"not_required_steps": 0
},
"created_at": "2026-10-08T02:13:04.940Z"
},
{
"id": "08a0a02b-27e3-430b-81a0-60a92af62afa",
"subject": {
"subject_type": "tool_instance",
"tool_instance": {
"id": 57,
"tool_id": 15,
"serial_number": "SN-2207",
"status": "available",
"location_id": 17
},
"tool": {
"id": 15,
"tool_number": "TL-022",
"name": "Digital Torque Wrench"
}
},
"binding_id": "163f9ab9-2a7c-4a25-b86a-2b49d20eedee",
"trigger_id": "464c52ac-f292-4b8b-8392-1a4b428fcff7",
"trigger_name": "Annual calibration",
"name": "Torque Wrench Calibration",
"status": "closed",
"outcome": "deferred",
"due_at": "2026-01-01T00:00:00.000Z",
"deferred_to_at": "2026-02-15T00:00:00.000Z",
"notes": "Calibration lab booked until February",
"closed_at": "2025-12-18T16:30:00.000Z",
"closed_by": "YfsK2SFZ8afndGo2u79ruB",
"run_ids": [],
"created_at": "2025-12-18T16:30:00.000Z"
}
],
"pagination": {
"limit": 1000,
"sort_by": null,
"sort_order": null,
"group_by": null,
"current_page_token": null,
"previous_page_token": null,
"next_page_token": null
}
}
GET https://api.epsilon3.io/v1/builds/maintenance/events
Retrieve maintenance events, newest first by when they were completed or closed, or else created. When authenticating with a user API key, it requires the builds:view (Builds View) permission.
Parameters
Parameters marked as repeatable match any of their values. Different parameters are combined with AND.
binding-id(query parameter): optional binding ID.outcome(query parameter): optional, repeatable.executed,deferred,canceledoraborted, orin_progressfor events whose run has not finished.status(query parameter): optional, repeatable.in_progress,completedorclosed.subject-type(query parameter): optional, repeatable.tool_instance,itemorlocation.tool-instance-id(query parameter): optional tool instance ID.item-id(query parameter): optional inventory item ID.location-id(query parameter): optional location ID.routine-name(query parameter): optional eventnameto match exactly. Events recorded for a routine are named after it.run-id(query parameter): optional run ID.q(query parameter): optional text to match against the event name, a tool instance's serial number or tool number, an item's tracking ID or part number, or a location name.
Paging Parameters
limit(query parameter): optional maximum number of events to return per page. Must be an integer between1and1000. Defaults to1000.page-token(query parameter): optional continuation token. To fetch the next page, pass back thenext_page_tokenfrom the previous response'spaginationblock, or itsprevious_page_tokento go back a page. Use the token only as returned; do not construct or parse it. Sending a token with a differentlimit, sort or filters returns400, so restart from the first page when they change.sort-by(query parameter): optional.recorded, the default order, orroutineto sort by eventname.sort-order(query parameter): optional.desc(the default) orasc.
The response's pagination block has limit, sort_by, sort_order, group_by, current_page_token, previous_page_token and next_page_token, which is null on the last page.
Response
- Status:
200 OK - Body:
dataholds the events. Every event has:id,name,run_idsandcreated_at.subject: the tool instance, item or location, in the same shape as on List Maintenance Statuses.status:in_progress: a run is under way on the occurrence.completed: the maintenance was done.closed: the occurrence was settled without the work being done: it was deferred or canceled, or its run ended without completing the work.
outcome:executed,deferred,canceled, orabortedfor a run that ended without completing the work. Acompletedevent is alwaysexecuted, and anin_progressevent also reportsexecuted.- These fields when they apply:
binding_id,trigger_idandtrigger_name: the routine binding and trigger the event was recorded for. Absent on unplanned maintenance.covered_by: on an event written because a higher trigger's completion reset this one, theevent_idandtrigger_nameof that completion.due_at: the target date the event satisfied.deferred_to_at: the date a deferral moved the occurrence to.maintained_atandmaintained_by: when and by whom the maintenance was done.closed_atandclosed_by: when and by whom the event was settled.notes.run: the run the maintenance was carried out under, in the same shape as a status'sin_progress_run.
Create Maintenance Events
Sample Request
import requests
import os
data = {
"events": [
{
"binding_id": "163f9ab9-2a7c-4a25-b86a-2b49d20eedee",
"trigger_id": "694a26b8-5f6e-4b65-9481-2b5f01bd88ac",
"tool_instance_id": 57,
"name": "Torque Wrench Calibration",
"maintained_at": "2026-11-03T14:00:00.000Z",
"notes": "Ratchet replaced"
},
{
"binding_id": "163f9ab9-2a7c-4a25-b86a-2b49d20eedee",
"trigger_id": "464c52ac-f292-4b8b-8392-1a4b428fcff7",
"tool_instance_id": 57,
"name": "Torque Wrench Calibration",
"outcome": "deferred",
"deferred_to_at": "2027-02-15T00:00:00.000Z"
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/builds/maintenance/events',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/builds/maintenance/events \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"events": [
{
"binding_id": "163f9ab9-2a7c-4a25-b86a-2b49d20eedee",
"trigger_id": "694a26b8-5f6e-4b65-9481-2b5f01bd88ac",
"tool_instance_id": 57,
"name": "Torque Wrench Calibration",
"maintained_at": "2026-11-03T14:00:00.000Z",
"notes": "Ratchet replaced"
},
{
"binding_id": "163f9ab9-2a7c-4a25-b86a-2b49d20eedee",
"trigger_id": "464c52ac-f292-4b8b-8392-1a4b428fcff7",
"tool_instance_id": 57,
"name": "Torque Wrench Calibration",
"outcome": "deferred",
"deferred_to_at": "2027-02-15T00:00:00.000Z"
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/maintenance/events"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
events = @(
@{
binding_id = "163f9ab9-2a7c-4a25-b86a-2b49d20eedee"
trigger_id = "694a26b8-5f6e-4b65-9481-2b5f01bd88ac"
tool_instance_id = 57
name = "Torque Wrench Calibration"
maintained_at = "2026-11-03T14:00:00.000Z"
notes = "Ratchet replaced"
},
@{
binding_id = "163f9ab9-2a7c-4a25-b86a-2b49d20eedee"
trigger_id = "464c52ac-f292-4b8b-8392-1a4b428fcff7"
tool_instance_id = 57
name = "Torque Wrench Calibration"
outcome = "deferred"
deferred_to_at = "2027-02-15T00:00:00.000Z"
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"events": [
{
"binding_id": "163f9ab9-2a7c-4a25-b86a-2b49d20eedee",
"trigger_id": "694a26b8-5f6e-4b65-9481-2b5f01bd88ac",
"tool_instance_id": 57,
"name": "Torque Wrench Calibration",
"maintained_at": "2026-11-03T14:00:00.000Z",
"notes": "Ratchet replaced"
},
{
"binding_id": "163f9ab9-2a7c-4a25-b86a-2b49d20eedee",
"trigger_id": "464c52ac-f292-4b8b-8392-1a4b428fcff7",
"tool_instance_id": 57,
"name": "Torque Wrench Calibration",
"outcome": "deferred",
"deferred_to_at": "2027-02-15T00:00:00.000Z"
}
]
}
Example Response Body
{
"events": [
{
"id": "e2b7c4d9-0f18-4a63-8e5b-71c3a9d6f402",
"subject": {
"subject_type": "tool_instance",
"tool_instance": {
"id": 57,
"tool_id": 15,
"serial_number": "SN-2207",
"status": "available",
"location_id": 17
},
"tool": {
"id": 15,
"tool_number": "TL-022",
"name": "Digital Torque Wrench"
}
},
"binding_id": "163f9ab9-2a7c-4a25-b86a-2b49d20eedee",
"trigger_id": "694a26b8-5f6e-4b65-9481-2b5f01bd88ac",
"trigger_name": "Every 500 check-outs",
"name": "Torque Wrench Calibration",
"status": "completed",
"outcome": "executed",
"maintained_at": "2026-11-03T14:00:00.000Z",
"closed_at": "2026-11-03T14:00:00.000Z",
"notes": "Ratchet replaced",
"run_ids": [],
"created_at": "2026-11-03T15:42:10.118Z"
},
{
"id": "4a9d2f61-c87e-4b30-95d2-0e6b1f7a3c58",
"subject": {
"subject_type": "tool_instance",
"tool_instance": {
"id": 57,
"tool_id": 15,
"serial_number": "SN-2207",
"status": "available",
"location_id": 17
},
"tool": {
"id": 15,
"tool_number": "TL-022",
"name": "Digital Torque Wrench"
}
},
"binding_id": "163f9ab9-2a7c-4a25-b86a-2b49d20eedee",
"trigger_id": "464c52ac-f292-4b8b-8392-1a4b428fcff7",
"trigger_name": "Annual calibration",
"name": "Torque Wrench Calibration",
"status": "closed",
"outcome": "deferred",
"due_at": "2027-01-01T00:00:00.000Z",
"deferred_to_at": "2027-02-15T00:00:00.000Z",
"closed_at": "2026-11-03T15:42:10.118Z",
"run_ids": [],
"created_at": "2026-11-03T15:42:10.118Z"
}
]
}
POST https://api.epsilon3.io/v1/builds/maintenance/events
Record one or more maintenance events against tool instances, items or locations. The events are recorded together: if any of them is rejected, none are recorded. The subjects' maintenance statuses are recomputed afterwards. When authenticating with a user API key, it requires the builds:operate (Builds Operate) permission. Deferring or canceling also requires builds:edit (Builds Edit): on the item's project for an item, and for the workspace for a tool instance or location. Naming a run requires runs:operate on its project.
Request Body
events: (required) Array of 1 to 100 events. Each contains:tool_instance_id,item_id,location_id: (required) exactly one of these, naming the subject.name: (required) String of up to 255 characters. Name an event after its routine, as the app does, so that it matches theroutine-namefilter.binding_id: (optional) ID of the routine binding the maintenance is for. The binding must cover the subject: bound to it directly, or to its tool or part. Without a binding the event records unplanned maintenance, which can only be executed.trigger_id: (optional) ID of the trigger whose work is recorded, from the binding's routine. Required when the routine has more than one trigger. Not allowed withoutbinding_id.status: (optional)completed(the default) orin_progress. Anin_progressevent names exactly one run that is still under way, and cannot setmaintained_at. When that run ends, the event is completed, or closed if the run did not complete the work. Only one event can be in progress for each binding, trigger and subject.outcome: (optional)executed(the default),deferredorcanceled.abortedis treated ascanceled. Only ascheduletrigger's occurrence can be deferred or canceled. A deferral or cancellation needs abinding_idand cannot be in progress or name a run.deferred_to_at: (required fordeferred, otherwise not allowed) ISO 8601 date in the future that the occurrence moves to.maintained_at: (optional) ISO 8601 date the maintenance was done, not in the future. Defaults to now for an executed event.notes: (optional) String of up to 4096 characters.run_ids: (optional) Array of up to 100 run IDs the maintenance was carried out under. For a trigger, each run must be of the trigger's procedure, and a trigger without a procedure takes no runs. A completed, executed event can only name runs that ended successfully. A run can only be linked to one event, though one request can link it to the events of several subjects.
A trigger with a procedure is recorded as executed by its run: an executed event for it must name the run. Deferring and canceling stay allowed.
Response
- Status:
201 Created - Body:
events, the recorded events in the order they were sent, in the same shape as List Maintenance Events.
Errors
400: an unknown subject, binding, trigger or run, a run that is not of the trigger's procedure, or a run already linked to an event.403: an item in a project where you lackbuilds:operate, a run in a project where you lackruns:operate, or deferring or canceling withoutbuilds:edit.422: an event that names no subject or several, an archived tool instance, a binding that does not cover the subject, a missingtrigger_id, an outcome or status that breaks the rules above, a second in-progress event, or an execution that skips the trigger's procedure.
Shipments
Get Shipments
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/shipments',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/shipments \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/shipments"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/shipments
Retrieve all inbound shipments defined in your workspace.
Example Response Body
{
"shipments": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"shipment_number": 1,
"status": "Shipped",
"items": [
{
"id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"part_revision_id": "NL1IeyfLXdvVaeWrbqqDg5",
"quantity": 10,
"purchase_order_id": "po_abc123",
"part": {
"part_id": "mo58Dbqw4UTxejDt5ZQz71",
"revision_id": "NL1IeyfLXdvVaeWrbqqDg5",
"part_number": "ANODE",
"revision": "1",
"name": "Anode"
}
}
],
"vendor_id": "vendor_abc",
"vendor_order_id": "VO-1234",
"carrier_id": "carrier_xyz",
"carrier_tracking_number": "1Z999AA10123456784",
"shipment_date": "2025-07-21T00:00:00.000Z",
"estimated_delivery_date": "2025-07-28T00:00:00.000Z",
"actual_delivery_date": null,
"notes": "Fragile components",
"attachments": [],
"created_at": "2025-07-20T12:00:00.000Z",
"updated_at": "2025-07-20T12:00:00.000Z"
},
...
]
}
Get Shipment
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
GET https://api.epsilon3.io/v1/builds/shipments/:shipmentId
Retrieve a single inbound shipment using its ID.
Parameters
shipmentId(path parameter): ID of the shipment being retrieved.
Example Response Body
{
"shipment": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"shipment_number": 1,
"status": "Shipped",
"items": [
{
"id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"part_revision_id": "NL1IeyfLXdvVaeWrbqqDg5",
"quantity": 10,
"purchase_order_id": "po_abc123",
"part": {
"part_id": "mo58Dbqw4UTxejDt5ZQz71",
"revision_id": "NL1IeyfLXdvVaeWrbqqDg5",
"part_number": "ANODE",
"revision": "1",
"name": "Anode"
},
"accepted_items": [
{
"item_id": "item_001",
"quantity": 8,
"created_at": "2025-07-25T10:00:00.000Z",
"location_id": 22,
"unit_cost_cents": 1500
}
],
"rejected_items": [
{
"item_id": "item_002",
"quantity": 2,
"created_at": "2025-07-25T10:00:00.000Z",
"notes": "Damaged in transit",
"rejection_reason": "Physical damage"
}
],
"additional_rejected_quantity": 0
}
],
"vendor_id": "vendor_abc",
"vendor_order_id": "VO-1234",
"carrier_id": "carrier_xyz",
"carrier_tracking_number": "1Z999AA10123456784",
"shipment_date": "2025-07-21T00:00:00.000Z",
"estimated_delivery_date": "2025-07-28T00:00:00.000Z",
"actual_delivery_date": null,
"notes": "Fragile components",
"attachments": [],
"created_at": "2025-07-20T12:00:00.000Z",
"updated_at": "2025-07-20T12:00:00.000Z"
}
}
Update Shipment
Sample Request
import requests
import os
data = {
"status": "Delivered",
"actual_delivery_date": "2025-07-28T14:30:00.000Z"
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"status": "Delivered",
"actual_delivery_date": "2025-07-28T14:30:00.000Z"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
status = "Delivered"
actual_delivery_date = "2025-07-28T14:30:00.000Z"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"status": "Delivered",
"actual_delivery_date": "2025-07-28T14:30:00.000Z"
}
PATCH https://api.epsilon3.io/v1/builds/shipments/:shipmentId
Updates values of one or more fields for a specific inbound shipment.
Parameters
shipmentId(path parameter): ID of the shipment to update.
Request Body
The request body should be a JSON object that contains the specific fields of the shipment to be updated. The request body must contain at least one field. Following fields are supported:
status: (optional) Shipment status. Must be one of:Shipped,Canceled,Delayed,Processing,Lost,Deliveredshipment_date: (optional) Shipment date in ISO 8601 formatestimated_delivery_date: (optional) Estimated delivery date in ISO 8601 formatactual_delivery_date: (optional) Actual delivery date in ISO 8601 formatnotes: (optional) Shipment notesitems: (optional) Object to manage shipment line items:added: (optional) Array of items to add. Each item contains:part_revision_id: (required) System generated ID of the part revisionquantity: (required) Number of units. Must be a positive numberpurchase_order_id: (optional) Purchase order ID, ornullto clearremoved: (optional) Array of shipment item IDs to removeupdated: (optional) Array of items to update. Each item contains:shipment_item_id: (required) ID of the shipment item to updatepart_revision_id: (optional) System generated ID of the part revisionquantity: (optional) Number of units. Must be a positive numberpurchase_order_id: (optional) Purchase order ID, ornullto clear
attachments: (optional) Array of attachment objects to add. Each object contains:attachment_id: (required) ID of the attachment
Response
- Status:
200 OK - Body: (empty)
Receive Inventory
Sample Request
import requests
import os
data = {
"accepted_items": [
{
"shipment_item_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"part_id": "mo58Dbqw4UTxejDt5ZQz71",
"part_revision_id": "NL1IeyfLXdvVaeWrbqqDg5",
"quantity": 8,
"location_id": 22,
"unit_cost_cents": 1500
}
],
"rejected_items": [
{
"shipment_item_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"part_id": "mo58Dbqw4UTxejDt5ZQz71",
"part_revision_id": "NL1IeyfLXdvVaeWrbqqDg5",
"quantity": 2,
"notes": "Damaged in transit",
"rejection_reason": "Physical damage"
}
]
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890/received-inventory',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890/received-inventory \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"accepted_items": [
{
"shipment_item_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"part_id": "mo58Dbqw4UTxejDt5ZQz71",
"part_revision_id": "NL1IeyfLXdvVaeWrbqqDg5",
"quantity": 8,
"location_id": 22,
"unit_cost_cents": 1500
}
],
"rejected_items": [
{
"shipment_item_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"part_id": "mo58Dbqw4UTxejDt5ZQz71",
"part_revision_id": "NL1IeyfLXdvVaeWrbqqDg5",
"quantity": 2,
"notes": "Damaged in transit",
"rejection_reason": "Physical damage"
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890/received-inventory"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
accepted_items = @(
@{
shipment_item_id = "f1e2d3c4-b5a6-7890-abcd-ef1234567890"
part_id = "mo58Dbqw4UTxejDt5ZQz71"
part_revision_id = "NL1IeyfLXdvVaeWrbqqDg5"
quantity = 8
location_id = 22
unit_cost_cents = 1500
}
)
rejected_items = @(
@{
shipment_item_id = "f1e2d3c4-b5a6-7890-abcd-ef1234567890"
part_id = "mo58Dbqw4UTxejDt5ZQz71"
part_revision_id = "NL1IeyfLXdvVaeWrbqqDg5"
quantity = 2
notes = "Damaged in transit"
rejection_reason = "Physical damage"
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"accepted_items": [
{
"shipment_item_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"part_id": "mo58Dbqw4UTxejDt5ZQz71",
"part_revision_id": "NL1IeyfLXdvVaeWrbqqDg5",
"quantity": 8,
"location_id": 22,
"unit_cost_cents": 1500
}
],
"rejected_items": [
{
"shipment_item_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"part_id": "mo58Dbqw4UTxejDt5ZQz71",
"part_revision_id": "NL1IeyfLXdvVaeWrbqqDg5",
"quantity": 2,
"notes": "Damaged in transit",
"rejection_reason": "Physical damage"
}
]
}
PATCH https://api.epsilon3.io/v1/builds/shipments/:shipmentId/received-inventory
Accept or reject inventory items received in an inbound shipment.
Parameters
shipmentId(path parameter): ID of the shipment.
Request Body
The request body should be a JSON object with the following fields. At least one of accepted_items, rejected_items, or rejected_quantities must be provided:
accepted_items: (optional) Array of items to accept into inventory. Each item contains:shipment_item_id: (required) ID of the shipment line itempart_id: (required) System generated ID of the partpart_revision_id: (required) System generated ID of the part revisionquantity: (required) Number of units to accept. Must be a positive numbertracking_id: (optional) Tracking identifier for the inventory itemlocation_id: (optional) ID of the inventory location to store the itemunit_cost_cents: (optional) Unit cost in cents. Must be a non-negative integernotes: (optional) Notes about the received item
rejected_items: (optional) Array of items to reject. Each item contains:shipment_item_id: (required) ID of the shipment line itempart_id: (required) System generated ID of the partpart_revision_id: (required) System generated ID of the part revisionquantity: (required) Number of units to reject. Must be a positive numbertracking_id: (optional) Tracking identifier for the inventory itemlocation_id: (optional) ID of the inventory locationunit_cost_cents: (optional) Unit cost in cents. Must be a non-negative integernotes: (optional) Notes about the rejected itemrejection_reason: (optional) Reason for rejection
rejected_quantities: (optional) Array of quantity-only rejections (no inventory item created). Each entry contains:shipment_item_id: (required) ID of the shipment line itemquantity: (required) Number of units to reject. Must be a positive numbernotes: (optional) Notes about the rejectionrejection_reason: (optional) Reason for rejection
Response
- Status:
200 OK - Body: (empty)
Link Run to Shipment Item
Sample Request
import requests
import os
data = {
"shipment_item_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"run_id": "run_abc123"
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890/link-run',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890/link-run \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"shipment_item_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"run_id": "run_abc123"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890/link-run"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
shipment_item_id = "f1e2d3c4-b5a6-7890-abcd-ef1234567890"
run_id = "run_abc123"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"shipment_item_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"run_id": "run_abc123"
}
PATCH https://api.epsilon3.io/v1/builds/shipments/:shipmentId/link-run
Link an inspection run to a specific item in an inbound shipment.
Parameters
shipmentId(path parameter): ID of the shipment.
Request Body
shipment_item_id: (required) ID of the shipment line item to linkrun_id: (required) ID of the run to link as an inspection run
Response
- Status:
200 OK - Body: (empty)
Get Shipment Activity
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890/activity',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890/activity \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890/activity"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/shipments/:shipmentId/activity
Retrieve the activity log for a specific inbound shipment. Results are paginated.
Parameters
shipmentId(path parameter): ID of the shipment.startBeforeId(query parameter, optional): Return activities with IDs less than this value, for pagination.
Example Response Body
{
"has_older": false,
"activities": [
{
"id": "42",
"timestamp": "2025-07-25T10:00:00.000Z",
"shipment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"action": "Status Updated",
"actor_type": "user",
"actor_id": "jane.doe@example.com",
"new_value": { "status": "Delivered" },
"old_value": { "status": "Shipped" }
},
{
"id": "41",
"timestamp": "2025-07-21T12:00:00.000Z",
"shipment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"action": "Created",
"actor_type": "user",
"actor_id": "jane.doe@example.com"
}
]
}
Outbound Shipments
Outbound shipments track goods sent to your customers, optionally linked to sale orders. Creating an outbound shipment and assigning inventory to its items are performed in the Epsilon3 web application; the API provides read access and the ability to update an existing shipment's details.
List Outbound Shipments
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/outbound-shipments',
params={'customer_id': 'd2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e'},
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
"https://api.epsilon3.io/v1/builds/outbound-shipments?customer_id=d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/outbound-shipments?customer_id=d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/builds/outbound-shipments?customer_id={customerId}&sale_order_id={saleOrderId}
Retrieve all outbound shipments in your workspace, optionally filtered by customer or sale order.
Parameters
customer_id(query parameter): optional customer ID. When set, only outbound shipments for that customer are returned.sale_order_id(query parameter): optional sale order ID. When set, only outbound shipments containing an item linked to that sale order are returned.
Example Response Body
{
"shipments": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"shipment_number": 1,
"status": "Shipped",
"shipment_type": "outbound",
"customer_id": "d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"customer_name": "Globex Corporation",
"carrier_id": "carrier_ups",
"carrier_tracking_number": "1Z999AA10123456784",
"reference_id": "REF-001",
"shipment_name": "Q3 Engine Delivery",
"shipment_date": "2025-07-21T00:00:00.000Z",
"ship_by_date": "2025-07-20T00:00:00.000Z",
"estimated_delivery_date": "2025-07-25T00:00:00.000Z",
"actual_delivery_date": null,
"notes": "Handle with care",
"packing_slip_notes": "Include calibration certificate",
"from_address": {
"name": "Headquarters",
"address_line_1": "123 Alpha Way",
"city": "Anytown",
"state_province": "CA",
"postal_code": "10101",
"country": "USA"
},
"to_address": {
"name": "Globex Corporation",
"address_line_1": "456 Beta Blvd",
"city": "Metropolis",
"state_province": "NY",
"postal_code": "20202",
"country": "USA"
},
"sale_order_numbers": ["SO-1"],
"items": [
{
"id": "si_abc123",
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"quantity": 2,
"sale_order_id": "9kLmN0pQ7rStUvWxYz1a2b",
"sale_order_line_id": "Lm3nOp4QrStUvWxYz12345",
"part": {
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"part_number": "ENGINE-1",
"revision": "C",
"name": "Rocket Engine"
}
}
],
"attachments": [],
"created_at": "2025-07-21T12:00:00.000Z",
"updated_at": "2025-07-25T10:00:00.000Z"
}
]
}
Response
- Status:
200 OK - Body: object with
shipments, an array of outbound shipment summaries. Each summary includes astatus(one ofDraft,Confirmed,Shipped,Delivered,Delayed,Lost,Canceled), thecustomer_idandcustomer_name, carrier and tracking details, addresses, linkedsale_order_numbers, and anitemsarray.
Get Outbound Shipment
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/outbound-shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/outbound-shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/outbound-shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
GET https://api.epsilon3.io/v1/builds/outbound-shipments/:shipmentId
Retrieve a single outbound shipment using its ID.
Parameters
shipmentId(path parameter): ID of the outbound shipment being retrieved.
Example Response Body
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"shipment_number": 1,
"status": "Shipped",
"shipment_type": "outbound",
"customer_id": "d2b3c4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"carrier_id": "carrier_ups",
"carrier_tracking_number": "1Z999AA10123456784",
"reference_id": "REF-001",
"shipment_name": "Q3 Engine Delivery",
"shipment_date": "2025-07-21T00:00:00.000Z",
"ship_by_date": "2025-07-20T00:00:00.000Z",
"estimated_delivery_date": "2025-07-25T00:00:00.000Z",
"actual_delivery_date": null,
"notes": "Handle with care",
"packing_slip_notes": "Include calibration certificate",
"from_address": {
"name": "Headquarters",
"address_line_1": "123 Alpha Way",
"city": "Anytown",
"state_province": "CA",
"postal_code": "10101",
"country": "USA"
},
"to_address": {
"name": "Globex Corporation",
"address_line_1": "456 Beta Blvd",
"city": "Metropolis",
"state_province": "NY",
"postal_code": "20202",
"country": "USA"
},
"items": [
{
"id": "si_abc123",
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"quantity": 2,
"sale_order_id": "9kLmN0pQ7rStUvWxYz1a2b",
"sale_order_line_id": "Lm3nOp4QrStUvWxYz12345",
"part": {
"part_id": "66qVqW9X5NKz7qPJhrlFtG",
"revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"part_number": "ENGINE-1",
"revision": "C",
"name": "Rocket Engine"
},
"reserved_quantity": 2,
"assigned_inventory": [
{
"item_id": "Uqo1eK43QThNE78Rl3HTco",
"quantity": 2
}
],
"shipped_items": [
{
"item_id": "Uqo1eK43QThNE78Rl3HTco",
"quantity": 2
}
]
}
],
"attachments": [],
"created_at": "2025-07-21T12:00:00.000Z",
"updated_at": "2025-07-25T10:00:00.000Z"
}
Response
- Status:
200 OK - Body: the full outbound shipment object. Its
itemsarray includes per-item detail such as the linkedsale_order_id/sale_order_line_id,reserved_quantity,assigned_inventory(the specific inventory items reserved for the item, each with anitem_idandquantity), andshipped_items(the specific inventory items shipped, populated once the shipment reachesShipped).
Update Outbound Shipment
Sample Request
import requests
import os
data = {
"status": "Shipped",
"carrier_tracking_number": "1Z999AA10123456784",
"actual_delivery_date": "2025-07-25T00:00:00.000Z"
}
requests.patch(
url='https://api.epsilon3.io/v1/builds/outbound-shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/builds/outbound-shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"status": "Shipped",
"carrier_tracking_number": "1Z999AA10123456784",
"actual_delivery_date": "2025-07-25T00:00:00.000Z"
}'
$apiUrl = "https://api.epsilon3.io/v1/builds/outbound-shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
status = "Shipped"
carrier_tracking_number = "1Z999AA10123456784"
actual_delivery_date = "2025-07-25T00:00:00.000Z"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"status": "Shipped",
"carrier_tracking_number": "1Z999AA10123456784",
"actual_delivery_date": "2025-07-25T00:00:00.000Z",
"items": {
"added": [
{
"part_revision_id": "W4e4WfDe6ZU8OZdvIaJIUw",
"quantity": 1,
"sale_order_id": "9kLmN0pQ7rStUvWxYz1a2b",
"sale_order_line_id": "Lm3nOp4QrStUvWxYz12345"
}
],
"updated": [
{
"shipment_item_id": "si_abc123",
"quantity": 3
}
],
"removed": ["si_def456"]
}
}
PATCH https://api.epsilon3.io/v1/builds/outbound-shipments/:shipmentId
Update values of one or more fields on an existing outbound shipment. Line item changes are supplied as a diff via the items object — only the items referenced in added, updated, and removed are affected.
Parameters
shipmentId(path parameter): ID of the outbound shipment being updated.
Request Body
The request body is a JSON object containing the fields to be updated. All fields are optional.
status: New shipment status. Must be one of:Draft,Confirmed,Shipped,Delivered,Delayed,Lost,Canceled.carrier_id: System generated ID of the carrier.carrier_tracking_number: Carrier tracking number.reference_id: Free-form external reference identifier.shipment_name: Human-readable name for the shipment.shipment_date: Ship date. Must be a valid ISO 8601 date.ship_by_date: Requested ship-by date. Must be a valid ISO 8601 date.estimated_delivery_date: Estimated delivery date. Must be a valid ISO 8601 date.actual_delivery_date: Actual delivery date. Must be a valid ISO 8601 date.from_address/to_address: Address objects. Each may includename,address_line_1,address_line_2,city,state_province,postal_code, andcountry.notes: Free-form notes.packing_slip_notes: Free-form notes to include on the packing slip.attachments: Array of attachment objects, each with anattachment_id(UUID of the uploaded attachment).items: Object describing a diff of the shipment's line items:added: Array of new items to add. Each containspart_revision_id(required),quantity(required), and optionalsale_order_idandsale_order_line_id.updated: Array of existing items to modify. Each containsshipment_item_id(required) plus any ofpart_revision_id,quantity,sale_order_id, orsale_order_line_id(the sale order fields acceptnullto unlink).removed: Array ofshipment_item_idstrings identifying items to remove.
Response
- Status:
200 OK - Body: (empty)
List Outbound Shipment Activity
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/outbound-shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890/activity',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/builds/outbound-shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890/activity \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/outbound-shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890/activity"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
Example Response Body
{
"has_older": false,
"activities": [
{
"id": "42",
"timestamp": "2025-07-25T10:00:00.000Z",
"shipment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"action": "Outbound Status Updated",
"actor_type": "user",
"actor_id": "jane.doe@example.com",
"new_value": { "status": "Shipped" },
"old_value": { "status": "Confirmed" }
},
{
"id": "41",
"timestamp": "2025-07-21T12:00:00.000Z",
"shipment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"action": "Created",
"actor_type": "user",
"actor_id": "jane.doe@example.com"
}
]
}
GET https://api.epsilon3.io/v1/builds/outbound-shipments/:shipmentId/activity?startBeforeId={startBeforeId}
List the most recent actions related to the specified outbound shipment. Use the optional startBeforeId query parameter to page through older activity.
Parameters
shipmentId(path parameter): ID of the outbound shipment for which to list activity.startBeforeId(query parameter): optional integer activity ID. When set, only activities with an ID less than this value are returned.
Response
- Status:
200 OK - Body: object with:
has_older: boolean indicating whether more activity entries are available before the oldest entry returned.activities: array of activity entries, ordered most recent first.
TIR Logs
List TIR Logs
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/builds/tir-logs',
params={'parent-item-id': 'xagtLhLxMEPLTgEHDNiEBa'},
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
"https://api.epsilon3.io/v1/builds/tir-logs?parent-item-id=xagtLhLxMEPLTgEHDNiEBa" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/builds/tir-logs?parent-item-id=xagtLhLxMEPLTgEHDNiEBa"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
Example Response Body
{
"tir_logs": [
{
"id": "0e9b3f7a-5d14-4c28-b6a1-8f2d4e7c9a30",
"tir_number": "TIR-00001",
"parent_item_id": "xagtLhLxMEPLTgEHDNiEBa",
"installation": {
"type": "item",
"item": {
"id": "Rk3vYpQ8mTzNw2cXbHdLqe",
"part_id": "u9HfWj4sKxVn7LrEoYtB1d",
"part_revision_id": "e5TgZc0aQmPy3NvXkJsW8h",
"tracking_id": "SN-0042",
"quantity": 1,
"status": "In Inventory"
},
"part": {
"id": "u9HfWj4sKxVn7LrEoYtB1d",
"revision_id": "e5TgZc0aQmPy3NvXkJsW8h",
"part_no": "VLV-200",
"revision": "A",
"name": "Relief Valve",
"project_id": null,
"allow_decimal_quantities": false,
"tracking": "serial",
"units": null
}
},
"quantity": 1,
"attachments": [],
"created_by": "jane.doe@example.com",
"created_at": "2026-10-18T09:05:00.000Z",
"status": "removed",
"parent_tir_log_event_id": "7c1d5a4e-2b0f-4f55-9a43-0d9a6e8c1b11"
},
{
"id": "7c1d5a4e-2b0f-4f55-9a43-0d9a6e8c1b11",
"tir_number": "TIR-00001",
"parent_item_id": "xagtLhLxMEPLTgEHDNiEBa",
"installation": {
"type": "item",
"item": {
"id": "Rk3vYpQ8mTzNw2cXbHdLqe",
"part_id": "u9HfWj4sKxVn7LrEoYtB1d",
"part_revision_id": "e5TgZc0aQmPy3NvXkJsW8h",
"tracking_id": "SN-0042",
"quantity": 1,
"status": "In Inventory"
},
"part": {
"id": "u9HfWj4sKxVn7LrEoYtB1d",
"revision_id": "e5TgZc0aQmPy3NvXkJsW8h",
"part_no": "VLV-200",
"revision": "A",
"name": "Relief Valve",
"project_id": null,
"allow_decimal_quantities": false,
"tracking": "serial",
"units": null
}
},
"quantity": 1,
"description": "Swapped in while the flight valve is on backorder",
"attachments": [],
"run_id": "b3f1c9d2-6a47-4e0c-8d15-52a7f3e9c604",
"run_info": {
"id": "b3f1c9d2-6a47-4e0c-8d15-52a7f3e9c604",
"code": "R-0017",
"name": "Propulsion Assembly",
"run_number": 17
},
"step_id": "a8d2e6f0-1c35-4b79-9e42-7f0b5c3d8a16",
"created_by": "jane.doe@example.com",
"created_at": "2026-10-14T16:20:00.000Z",
"status": "installed",
"parent_ref_des": { "id": "Qw7nLmXc2VbHs9TfDgKzPa", "name": "V2" },
"weight": 120.5,
"weight_unit": "g",
"weight_grams": 120.5
}
],
"pagination": {
"limit": 1000,
"total": 2,
"next_page_token": null
}
}
GET https://api.epsilon3.io/v1/builds/tir-logs
Retrieve the log of temporary installations (TIRs), newest event first. A TIR is opened by an installed event, which is assigned a tir_number. Later events (removed, lost, already_removed, waived and voided) are recorded against that install and carry its tir_number. Requires the manufacturing module. When authenticating with a user API key, it also requires the builds:view (Builds View) permission.
Parameters
id(query parameter): optional event ID. Repeat the parameter to include several events. Without it, events with any ID are returned.parent-item-id(query parameter): optional ID of the item the hardware was installed into. Repeat the parameter to include several items. Without it, events for every item are returned.status(query parameter): optional event status. Repeat the parameter to match any of several statuses. Without it, events of every status are returned. Must be one of:installed: the install event that opens a TIR.removed: the hardware was taken back out of the parent item.lost: the hardware was lost. It counts as no longer installed.already_removed: the hardware was recorded as already out of the parent item.waived: the installation was approved to stay in place. It closes out the quantity without reducing what is installed.voided: an earlier event was negated in full.actively_installed: not an event status. Returns the install events that are not voided and still have hardware installed, including waived installs. On these eventsquantityis the quantity still installed instead of the quantity originally installed, andunresolved_quantityis the quantity not yet resolved by any later event. May be combined with the statuses above.
weight-unit(query parameter): optional unit for the returnedweight. Must be one ofg,kg,ozorlb. Defaults to the workspace's small unit. See Weight Units.
Paging Parameters
limit(query parameter): optional maximum number of events to return per page. Must be an integer between1and1000. Defaults to1000.page-token(query parameter): optional continuation token. To fetch the next page, pass back thenext_page_tokenreturned in the previous response'spaginationblock. Use the token only as returned — do not construct or parse it. A token marks a position in the list, so events recorded while you page can shift later pages and cause an event to be returned again. An invalid token returns400.
Response
- Status:
200 OK - Body: object with:
tir_logs: array of events. Every event has:id,tir_number,parent_item_id,quantity,created_by,created_at(ISO 8601), andattachments(array ofattachment_id,name,content_type).status: the event's status.installation: what was installed, discriminated ontype:item:itemandpartobjects describing the installed item and its part. When the caller cannot view the item's project, both are reduced to just theirid.tool_instance:tool_instanceandtoolobjects.free_text: aname.
description,run_id,run_infoandstep_id: present when the event has them.run_infois omitted when the caller cannot view the run's project.
installedevents also have the optionalparent_ref_des(idandnameof the reference designator). An install with a recorded weight also hasweightand itsweight_unit, plus the deprecatedweight_grams(the same weight in grams).- Every other event also has
parent_tir_log_event_id, the ID of the event it was recorded against. pagination: object withlimit,total(the number of events matching the filters), andnext_page_token(nullon the last page).
An unsupported query parameter, or an invalid status or limit, returns 422.
Chat API
Send and manage chat messages on runs, work orders, purchase orders, sale orders, invoices, and journal entries.
List chat messages
This endpoint retrieves chat messages for a given entity with pagination. Messages are returned in chronological order (oldest first).
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/chat/messages?entity_type=run&entity_id=abc123&limit=50&offset=0',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
)
curl -X GET \
"https://api.epsilon3.io/v1/chat/messages?entity_type=run&entity_id=abc123&limit=50&offset=0" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/chat/messages?entity_type=run&entity_id=abc123&limit=50&offset=0"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/chat/messages
| Query parameter | Value |
|---|---|
| entity_type | (optional) string - "run" | "work_order" | "purchase_order" | "sale_order" | "invoice" | "journal_entry" |
| entity_id | (optional) string - the ID of the entity |
| limit | (optional) integer - number of messages to return (1-100, default 50) |
| offset | (optional) integer - number of messages to skip (default 0) |
Example Response Body
{
"data": [
{
"id": "chat_abc123def456ghi789jk",
"entity_type": "run",
"entity_id": "abc123",
"sender_id": "user_123",
"message": "Hello team",
"mentions": [
{
"email": "user@example.com",
"username": "jdoe"
}
],
"attachments": [],
"status": "sent",
"created_at": "2025-01-01T00:00:00.000Z",
"updated_at": "2025-01-01T00:00:00.000Z"
}
],
"total_count": 1,
"current_count": 1,
"offset": 0,
"next_page": null
}
Send a chat message
This endpoint sends a new chat message to an entity. At least one of message or attachments must be provided.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/chat/messages',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"entity_type": "run",
"entity_id": "abc123",
"message": "Hello @jdoe",
"mentions": [
{
"email": "user@example.com",
"username": "jdoe"
}
],
"attachments": [
{
"attachment_id": "att_123",
"name": "report.pdf",
"content_type": "application/pdf"
}
]
}
)
curl -X POST \
https://api.epsilon3.io/v1/chat/messages \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"entity_type": "run",
"entity_id": "abc123",
"message": "Hello @jdoe",
"mentions": [
{
"email": "user@example.com",
"username": "jdoe"
}
],
"attachments": [
{
"attachment_id": "att_123",
"name": "report.pdf",
"content_type": "application/pdf"
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/chat/messages"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"entity_type" = "run"
"entity_id" = "abc123"
"message" = "Hello @jdoe"
"mentions" = @(
@{
"email" = "user@example.com"
"username" = "jdoe"
}
)
"attachments" = @(
@{
"attachment_id" = "att_123"
"name" = "report.pdf"
"content_type" = "application/pdf"
}
)
} | ConvertTo-Json -Depth 3
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"entity_type": "run" | "work_order" | "purchase_order" | "sale_order" | "invoice" | "journal_entry",
"entity_id": string,
"message": (optional) string,
"mentions": (optional) Array<Mention>,
"attachments": (optional) Array<Attachment>
}
POST https://api.epsilon3.io/v1/chat/messages
Example Response Body
{
"id": "chat_abc123def456ghi789jk",
"message": {
"id": "chat_abc123def456ghi789jk",
"entity_type": "run",
"entity_id": "abc123",
"sender_id": "user_123",
"message": "Hello @jdoe",
"mentions": [
{
"email": "user@example.com",
"username": "jdoe"
}
],
"attachments": [
{
"attachment_id": "att_123",
"name": "report.pdf",
"content_type": "application/pdf"
}
],
"status": "sent",
"created_at": "2025-01-01T00:00:00.000Z",
"updated_at": "2025-01-01T00:00:00.000Z"
}
}
Edit a chat message
This endpoint edits an existing chat message. Only the original sender can edit their message. At least one of message or attachments must be provided.
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v1/chat/messages/chat_abc123def456ghi789jk',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"message": "Updated message",
"mentions": [],
"attachments": []
}
)
curl -X PATCH \
https://api.epsilon3.io/v1/chat/messages/chat_abc123def456ghi789jk \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"message": "Updated message",
"mentions": [],
"attachments": []
}'
$apiUrl = "https://api.epsilon3.io/v1/chat/messages/chat_abc123def456ghi789jk"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"message" = "Updated message"
"mentions" = @()
"attachments" = @()
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
Request Body
{
"message": (optional) string,
"mentions": (optional) Array<Mention>,
"attachments": (optional) Array<Attachment>
}
PATCH https://api.epsilon3.io/v1/chat/messages/:messageId
Example Response Body
{
"message": {
"id": "chat_abc123def456ghi789jk",
"entity_type": "run",
"entity_id": "abc123",
"sender_id": "user_123",
"message": "Updated message",
"mentions": [],
"attachments": [],
"status": "sent",
"created_at": "2025-01-01T00:00:00.000Z",
"updated_at": "2025-01-01T00:01:00.000Z"
}
}
Delete a chat message
This endpoint soft-deletes a chat message. Only the original sender can delete their message.
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/chat/messages/chat_abc123def456ghi789jk',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
)
curl -X DELETE \
"https://api.epsilon3.io/v1/chat/messages/chat_abc123def456ghi789jk" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/chat/messages/chat_abc123def456ghi789jk"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers $headers
DELETE https://api.epsilon3.io/v1/chat/messages/:messageId
Complementary schemas
ChatMessage
{
"id": string,
"entity_type": "run" | "work_order" | "purchase_order" | "sale_order" | "invoice" | "journal_entry",
"entity_id": string,
"sender_id": string,
"message": string,
"mentions": Array<Mention>,
"attachments": Array<Attachment>,
"status": "sent",
"created_at": string,
"updated_at": string
}
Mention
{
"email": string,
"username": string
}
Attachment
{
"attachment_id": string,
"name": string,
"content_type": string
}
Commanding API
Epsilon3’s commanding API supports creating, updating, retrieving, or deleting commands. Uploading commands to the commanding API allows the user to do the following in Epsilon3:
- Search for and insert commands when creating a procedure
- Send commands to external systems with optional arguments in a running procedure
Create or update a command
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/commands',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"name": "test.command",
"dictionary_id": 1,
"arguments": [
{
"name": "arg1",
"type": "enum",
"values": [
{
"string": "OFF",
"value": 0
},
{
"string": "ON",
"value": 1
}]
},
{
"name": "arg2",
"type": "int",
"optional": true
}
]}
)
curl -X POST \
https://api.epsilon3.io/v1/commands \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"name": "test.command",
"dictionary_id": 1,
"arguments": [
{
"name": "arg1",
"type": "enum",
"values": [
{
"string": "OFF",
"value": 0
},
{
"string": "ON",
"value": 1
}
]
},
{
"name": "arg2",
"type": "int",
"optional": true
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/commands"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"name": "test.command",
"dictionary_id": 1,
"arguments": [
{
"name": "arg1",
"type": "enum",
"values": [
{
"string": "OFF",
"value": 0
},
{
"string": "ON",
"value": 1
}
]
},
{
"name": "arg2",
"type": "int",
"optional": true
}
]
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body (Command Object)
{
"name": string (1-255 characters),
"dictionary_id": integer (optional),
"description": string (optional, 1-255 characters),
"arguments": array (optional, max 100 items),
"expiration_seconds": integer (optional, 1-18000)
}
Argument Object
{
"name": string (1-255 characters),
"type": string (float, int, string, enum, or file),
"optional": boolean (optional, defaults to false),
"values": array (optional, max 100 items)
}
POST https://api.epsilon3.io/v1/commands
The name field should be unique per dictionary. The arguments field contains an array of one or more Argument objects. The optional dictionary_id field allows the command to be related to a dictionary. If no dictionary_id is provided, the command will be related to the default dictionary. The optional description field allows a human-readable description for the command.
The optional expiration_seconds field enables asynchronous command execution. When set, commands will be executed asynchronously, allowing for long-running operations that can take minutes or hours to complete. The value specifies the maximum execution timeout in seconds (1 to 18000, maximum 5 hours). See the Realtime Commanding Section for details.
Supported argument types:
floatintstringenumfile
Enum Object
{
"string": string (1-255 characters),
"value": integer
}
Example Request Body (No Arguments)
{
"name": "test.hatch.1.open"
}
Example Request Body (with a dictionary)
{
"name": "test.hatch.1.open",
"dictionary_id": 5
}
Example Request Body (Float)
{
"name": "test.pressure.level",
"arguments": [
{
"name": "psi",
"type": "float"
}
]
}
Example Request Body (Int)
{
"name": "test.timer",
"arguments": [
{
"name": "seconds",
"type": "int"
}
]
}
Example Request Body (String)
{
"name": "test.alert",
"arguments": [
{
"name": "message",
"type": "string"
}
]
}
Example Request Body (Enum)
{
"name": "test.power.switch",
"arguments": [
{
"name": "state",
"type": "enum",
"values": [
{
"string": "OFF",
"value": 0
},
{
"string": "ON",
"value": 1
}
]
}
]
}
Example Request Body (File)
{
"name": "test.config.file",
"arguments": [
{
"name": "config.file",
"type": "file"
}
]
}
Response Body
{
"id": integer,
"name": string,
"dictionary_id": integer,
"description": string (optional),
"arguments": array (optional),
"created_at": string, timestamp in ISO-8601 format
}
Bulk create or update commands
Sample Request
import requests
import os
requests.put(
url='https://api.epsilon3.io/v1/commands/bulk',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"commands": [
{
"name": "test.hatch.open",
"dictionary_id": 1
},
{
"name": "test.power.switch",
"arguments": [
{
"name": "state",
"type": "enum",
"values": [
{ "string": "OFF", "value": 0 },
{ "string": "ON", "value": 1 }
]
}
]
}
]
}
)
curl -X PUT \
https://api.epsilon3.io/v1/commands/bulk \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"commands": [
{
"name": "test.hatch.open",
"dictionary_id": 1
},
{
"name": "test.power.switch",
"arguments": [
{
"name": "state",
"type": "enum",
"values": [
{ "string": "OFF", "value": 0 },
{ "string": "ON", "value": 1 }
]
}
]
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/commands/bulk"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"commands" = @(
@{
"name" = "test.hatch.open"
"dictionary_id" = 1
},
@{
"name" = "test.power.switch"
"arguments" = @(
@{
"name" = "state"
"type" = "enum"
"values" = @(
@{ "string" = "OFF"; "value" = 0 },
@{ "string" = "ON"; "value" = 1 }
)
}
)
}
)
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri $apiUrl -Method Put -Headers $headers -Body $body
Request Body
{
"commands": array (1 to 1000 items) [
{
"name": string (1-255 characters),
"dictionary_id": integer (optional),
"description": string (optional, 1-255 characters),
"expiration_seconds": integer (optional, 1-18000),
"arguments": array (optional, max 100 items)
}
]
}
PUT https://api.epsilon3.io/v1/commands/bulk
Creates or updates 1 to 1000 commands in a single request. Each command in the commands array accepts the same fields as the single create endpoint. If a command with the same name and dictionary_id already exists, it will be updated.
Response Body
{
"commands": [
{
"id": integer,
"name": string,
"dictionary_id": integer,
"description": string (optional),
"arguments": array (optional),
"created_at": string, timestamp in ISO-8601 format
}
]
}
List all available commands
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/commands',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/commands \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/commands"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/commands
Sample Request with Query Parameters
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/commands',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
params={
"dictionary-group-ids": "1,5,28"
}
)
curl -X GET \
"https://api.epsilon3.io/v1/commands?dictionary-group-ids=1,5,28" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/commands?dictionary-group-ids=1,5,28"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/commands?dictionary-group-ids={dictionary-group-ids}
The optional URL parameter dictionary-group-ids may be provided to filter commands by dictionary groups. The value should be a comma-separated list of dictionary group IDs (max 1000). The response will only contain commands that belong to at least one of the specified dictionary groups.
Response Body (List of Command Objects)
[{
"id": integer,
"name": string,
"dictionary_id": integer,
"description": string (optional)
"arguments": array (optional)
"created_at": string, timestamp in ISO-8601 format
}]
Retrieve a command by name
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/commands',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
params={
"name": "test.command",
"dictionary-id": 1
}
)
curl -X GET \
"https://api.epsilon3.io/v1/commands/query?name=test.command&dictionary-id=1" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/commands/query?name=test.command&dictionary-id=1"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/commands/query?name={name}&dictionary-id={dictionary-id}
The URL parameter name is required and must be URL encoded. An optional dictionary-id may be provided to choose a specific dictionary. If dictionary-id is not included, the default dictionary will be used.
Delete commands
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/commands',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"ids": [1, 2, 3]
}
)
curl -X DELETE \
https://api.epsilon3.io/v1/commands \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"ids": [1, 2, 3]
}'
$apiUrl = "https://api.epsilon3.io/v1/commands"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"ids": @(1, 2, 3)
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers $headers -Body $body
DELETE https://api.epsilon3.io/v1/commands/
Request Body
{
"ids": array (integer, 1 to 1000 items)
}
Example Request Body
{
"ids": [123, 456]
}
Custom Fields API
Manage custom fields that can be attached to entities such as Issues, Parts, Inventory Items, and more.
List Custom Fields
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/fields',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/fields \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/fields"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/fields
Retrieve all custom field definitions available in your workspace.
Example Response Body
{
"data": [
{
"id": "fld_1",
"team_id": "team123",
"field_id": "external_id",
"name": "External Id",
"type": "Text",
"description": "External reference",
"created_at": "2025-08-21T16:46:47.352Z",
"updated_at": "2025-08-21T16:46:47.352Z"
},
{
"id": "fld_2",
"team_id": "team123",
"field_id": "priority",
"name": "Priority",
"type": "Custom List",
"metadata": { "options": ["High", "Medium", "Low"] },
"created_at": "2025-08-21T18:27:52.802Z",
"updated_at": "2025-08-21T18:27:52.802Z"
}
]
}
Create Custom Fields
Request URL
POST https://api.epsilon3.io/v1/fields
Example Request Body
{
"fields": [
{
"name": "External Id",
"field_id": "external_id",
"type": "Text",
"description": "External reference"
},
{
"name": "Priority",
"field_id": "priority",
"type": "Custom List",
"metadata": { "options": ["High", "Medium", "Low"] }
}
]
}
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/fields',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"fields": [
{ "name": "External Id", "field_id": "external_id", "type": "Text" },
{ "name": "Priority", "field_id": "priority", "type": "Custom List", "metadata": { "options": ["High", "Medium", "Low"] } }
]
}
)
curl -X POST \
https://api.epsilon3.io/v1/fields \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"fields": [
{ "name": "External Id", "field_id": "external_id", "type": "Text" },
{ "name": "Priority", "field_id": "priority", "type": "Custom List", "metadata": { "options": ["High", "Medium", "Low"] } }
]
}'
$apiUrl = "https://api.epsilon3.io/v1/fields"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
fields = @(
@{ name = "External Id"; field_id = "external_id"; type = "Text" },
@{ name = "Priority"; field_id = "priority"; type = "Custom List"; metadata = @{ options = @("High","Medium","Low") } }
)
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Example Response Body
{
"fields": [
{
"id": "fld_1",
"team_id": "team123",
"field_id": "external_id",
"name": "External Id",
"type": "Text",
"description": "External reference",
"created_at": "2025-08-21T16:46:47.352Z",
"updated_at": "2025-09-14T16:46:47.352Z"
},
{
"id": "fld_2",
"team_id": "team123",
"field_id": "priority",
"name": "Priority",
"type": "Custom List",
"metadata": { "options": ["High", "Medium", "Low"] },
"created_at": "2025-08-21T18:27:52.802Z",
"updated_at": "2025-09-14T16:46:47.352Z"
}
]
}
POST https://api.epsilon3.io/v1/fields
Add one or more custom fields to your workspace.
Request Body
fields: an array (1–1000) of objects, each with the following fields
name: (required) string, 1–255 charactersfield_id: (required) string, 1–255 characters (unique across all custom fields in the workspace)type: (required) one ofText,Number,Date,Time,Date Time,Checkbox,List,Custom List,External Datadescription: (optional) string, up to 10,000 charactersmetadata: (optional unless required bytype) object- For
Number: (optional){ "units": string } - For
List: (required){ "list_id": string, "allow_multiple": (optional) boolean }— theidof an existing Predefined List - For
Custom List: (required){ "options": string[], "allow_multiple": (optional) boolean } - For
External Data: (required){ "external_data_type": string, "dictionary_id": number }
- For
List and Custom List differ in where the selectable values live. A Custom List field owns its
own options. A List field owns none — list_id points at a shared Predefined List,
and the values come from there.
Set allow_multiple to true on a List or Custom List field to let it hold several selections
at once. Each form the field is on can override this setting for that form.
Additional Examples
{
"fields": [
{ "name": "Mass", "field_id": "mass", "type": "Number", "metadata": { "units": "kg" } },
{ "name": "Status", "field_id": "status", "type": "List", "metadata": { "list_id": "issue_status" } },
{ "name": "Priority", "field_id": "priority", "type": "Custom List", "metadata": { "options": ["High","Medium","Low"] } },
{ "name": "Vendor", "field_id": "vendor", "type": "External Data", "metadata": { "external_data_type": "supplier", "dictionary_id": 42 } }
]
}
Response
The response status code will be 201 and return { "fields": [...] }.
Update Custom Field
Example Request Body
{
"name": "Priority Level",
"description": "Task priority",
"field_id": "priority"
}
Example Response Body
{
"id": "fld_2",
"team_id": "team123",
"field_id": "priority",
"name": "Priority Level",
"type": "Custom List",
"metadata": { "options": ["High", "Medium", "Low"] },
"description": "Task priority",
"created_at": "2025-08-21T18:27:52.802Z",
"updated_at": "2025-09-14T16:46:47.352Z"
}
PATCH https://api.epsilon3.io/v1/fields/:id
Updates values of one or more fields for a specific custom field. The type cannot be changed. The request body must contain at least one field.
Request Body
name: (optional) string, 1–255 charactersdescription: (optional) string, up to 10,000 charactersfield_id: (optional) string, 1–255 charactersmetadata: (optional) object that must conform to the field's existingtype- For
Number:{ "units": string } - For
List:{ "list_id": string, "allow_multiple": (optional) boolean }— theidof an existing Predefined List - For
Custom List:{ "options": string[], "allow_multiple": (optional) boolean } - For
External Data:{ "external_data_type": string, "dictionary_id": number }
- For
Changing the selectable values of a list field
This depends on the field's type, and only one of the two is done through this endpoint.
Custom List— the options belong to the field. Send the full replacement set here:{ "metadata": { "options": ["High", "Medium", "Low", "Critical"] } }.List— the options do not belong to the field.metadata.list_idis only a pointer to a shared Predefined List, so there is nothing on the field to edit. Add or change values with Update a Predefined List. Sendingmetadata.optionsto aListfield does not add values — it overwriteslist_idand detaches the field from its Predefined List. Usemetadata.list_idhere only to point the field at a different list.
Response
The response status code will be 200 and return the updated field object.
List Forms
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/forms',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
params={ 'entity': 'Issue' }
)
curl -X GET \
"https://api.epsilon3.io/v1/forms?entity=Issue" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/forms?entity=Issue"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/forms
Retrieve all custom field forms available in your workspace. Optionally filter by one or more entity types.
Query Parameters
entity(optional): a single entity type to filter by — one ofCustomer,Hazard,InventoryItem,Issue,Part,Procedure,PurchaseOrder,PurchaseOrderLine,Requirement,Risk,Run,SaleOrder,ServiceWorkOrder,Shipment,Tool,ToolInstance,User,Vendor,WorkOrder.entities(optional): filter by more than one entity type, passed as a repeated query parameter (e.g.?entities=Issue&entities=Part). Each value must be one of the types listed forentity. May be combined withentity; duplicate types are ignored.
Example Response Body
{
"data": [
{
"id": "8c2a1f4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b",
"team_id": "team123",
"name": "Issue",
"entity_type": "Issue",
"fa_icon": "fa-circle-exclamation",
"archived": false,
"fields": [
{
"id": "fld_sys_1",
"team_id": "team123",
"field_id": "external_id",
"name": "External Id",
"type": "Text",
"description": "External reference",
"required": false
},
{
"id": "fld_sys_2",
"team_id": "team123",
"field_id": "priority",
"name": "Priority",
"type": "Custom List",
"description": "Task priority",
"metadata": { "options": ["High", "Medium", "Low"] },
"required": true,
"default_value": "Medium",
"display": { "show_label": true }
}
]
},
{
"id": "3f7b9c1d-2e4a-4b6c-8d9e-0f1a2b3c4d5e",
"team_id": "team123",
"name": "Inventory Item",
"entity_type": "InventoryItem",
"fa_icon": "fa-box",
"archived": false,
"fields": [
{
"id": "fld_sys_4",
"team_id": "team123",
"field_id": "coating",
"name": "Coating",
"type": "Custom List",
"description": null,
"metadata": { "options": ["Anodized", "Painted", "Bare"] },
"required": false
},
{
"id": "fld_sys_5",
"team_id": "team123",
"field_id": "coating_notes",
"name": "Coating Notes",
"type": "Text",
"description": null,
"required": true,
"default_value": "None",
"display": { "show_label": true },
"rule": {
"action": "SHOW",
"expression": {
"field_system_id": "fld_sys_4",
"field_id": "coating",
"field_name": "Coating",
"operator": "INCLUDES",
"operand": ["Anodized", "Painted"]
}
}
}
]
}
]
}
Retrieve Form
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/forms/8c2a1f4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/forms/8c2a1f4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/forms/8c2a1f4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/forms/:id
Retrieve a single custom field form by its id, including its fields. Returns 404 if no form with that id exists in your workspace, and 422 if id is not a UUID.
Example Response Body
{
"id": "8c2a1f4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b",
"team_id": "team123",
"name": "Issue",
"entity_type": "Issue",
"fa_icon": "fa-circle-exclamation",
"archived": false,
"fields": [
{
"id": "fld_sys_1",
"team_id": "team123",
"field_id": "external_id",
"name": "External Id",
"type": "Text",
"description": "External reference",
"required": false
}
]
}
Form properties
id: system generated ID of the form. Use it asform_idwhen configuring fields.name,fa_icon: display name and Font Awesome icon shown in the app.entity_type: the entity type the form applies to.archived: whether the form has been archived. Archived forms are still returned so records that use them keep resolving.layout: (optional) column configuration for the form's layout regions.fields: the custom fields attached to the form. Each entry is the field's definition (as returned by List Custom Fields) plus the per-form properties below.
Field properties on a form
required: whether a value is required before a record can be saved.default_value: (optional) value pre-filled on new records. Same shape asvalue.recordedin Recorded Custom Field Values. Omitted when no default is set.rule: (optional) the field's display rule. Omitted when the field has none.display: (optional){ "show_label": boolean, "column_label": string, "hidden": boolean }.layout: (optional) the field's position and size in the form layout.input_rules,expression_tokens: (optional) computed-field configuration authored in the settings UI.
Set required, default_value, rule, and display with Update Form Field.
Add Fields to Form
Request URL
POST https://api.epsilon3.io/v1/forms-fields
Example Request Body
{
"form_id": "3f7b9c1d-2e4a-4b6c-8d9e-0f1a2b3c4d5e",
"field_ids": ["fld_sys_4", "fld_sys_5"]
}
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/forms-fields',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={ "form_id": "3f7b9c1d-2e4a-4b6c-8d9e-0f1a2b3c4d5e", "field_ids": ["fld_sys_4", "fld_sys_5"] }
)
curl -X POST \
https://api.epsilon3.io/v1/forms-fields \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{ "form_id": "3f7b9c1d-2e4a-4b6c-8d9e-0f1a2b3c4d5e", "field_ids": ["fld_sys_4", "fld_sys_5"] }'
$apiUrl = "https://api.epsilon3.io/v1/forms-fields"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
form_id = "3f7b9c1d-2e4a-4b6c-8d9e-0f1a2b3c4d5e"
field_ids = @("fld_sys_4", "fld_sys_5")
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
POST https://api.epsilon3.io/v1/forms-fields
Attach one or more custom field definitions to a form so they appear on records of that form's entity type. A newly attached field has no display rule, is not required, and has no default value; configure those with Update Form Field.
Request Body
form_id: (required)idof one of your workspace's forms, from List Forms. Must be a UUID.field_ids: (required) array of 1–100 custom field definition ids from your workspace. Use theidreturned by List Custom Fields, not the user-definedfield_id.
Response
The response status code will be 204 with no body. Fields already attached to the form are left unchanged, including their existing configuration, so the request can safely be repeated.
Returns 404 with { "message": "ID '<form_id>' Not Found" } when form_id is not one of your workspace's forms, and 400 with { "error": "Invalid field ids in request: <ids>" } when any of field_ids is not one of your workspace's fields. In that case no fields are attached.
Update Form Field
Request URL
PUT https://api.epsilon3.io/v1/forms-fields
Example Request Body
{
"form_id": "3f7b9c1d-2e4a-4b6c-8d9e-0f1a2b3c4d5e",
"field_id": "fld_sys_5",
"required": true,
"default_value": "None",
"display": { "show_label": true },
"rule": {
"action": "SHOW",
"expression": {
"field_system_id": "fld_sys_4",
"operator": "INCLUDES",
"operand": ["Anodized", "Painted"]
}
}
}
Sample Request
import requests
import os
requests.put(
url='https://api.epsilon3.io/v1/forms-fields',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"form_id": "3f7b9c1d-2e4a-4b6c-8d9e-0f1a2b3c4d5e",
"field_id": "fld_sys_5",
"required": True,
"rule": {
"action": "SHOW",
"expression": { "field_system_id": "fld_sys_4", "operator": "INCLUDES", "operand": ["Anodized", "Painted"] }
}
}
)
curl -X PUT \
https://api.epsilon3.io/v1/forms-fields \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"form_id": "3f7b9c1d-2e4a-4b6c-8d9e-0f1a2b3c4d5e",
"field_id": "fld_sys_5",
"required": true,
"rule": {
"action": "SHOW",
"expression": { "field_system_id": "fld_sys_4", "operator": "INCLUDES", "operand": ["Anodized", "Painted"] }
}
}'
$apiUrl = "https://api.epsilon3.io/v1/forms-fields"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
form_id = "3f7b9c1d-2e4a-4b6c-8d9e-0f1a2b3c4d5e"
field_id = "fld_sys_5"
required = $true
rule = @{
action = "SHOW"
expression = @{ field_system_id = "fld_sys_4"; operator = "INCLUDES"; operand = @("Anodized", "Painted") }
}
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri $apiUrl -Method Put -Headers $headers -Body $body
PUT https://api.epsilon3.io/v1/forms-fields
Configure how a field behaves on one form: whether it is required, its default value, its display options, and the display rule that shows or hides it. The field must already be attached to the form. The same field can be configured differently on each form it is attached to.
Request Body
form_id: (required)idof one of your workspace's forms. Must be a UUID.field_id: (required)idof the custom field definition (theidfrom List Custom Fields, not the user-definedfield_id).required: (optional) boolean. Whentrue, the field must be filled in before a record can be saved. Defaults tofalse.default_value: (optional) value pre-filled on new records. Same shape asvalue.recordedin Recorded Custom Field Values for the field's type.nullclears it.rule: (optional) display rule. Omit to remove the field's rule.display: (optional) object. When omitted, the field's current display settings are kept. When present it replaces the wholedisplayobject, so include every setting you want to keep.show_label: (optional) boolean. Whether the field's name is shown next to its value.hidden: (optional) boolean. Whentrue, the field is hidden on the form regardless of any rule.column_label: (optional) string. Shorter name used as the column header where the field is shown in a table.
input_rules,expression_tokens: (optional) computed-field configuration authored in the settings UI. Not covered here.
Response
The response status code will be 204 with no body. Returns 404 with { "message": "ID '<form_id>' Not Found" } when form_id is not one of your workspace's forms, and 400 with { "error": "Invalid field ids in request: <field_id>" } when field_id is not one of your workspace's fields. Returns 400 with message set to Field is not attached to the form when the field exists but has not been added to the form. An invalid rule returns 400 with a message describing the problem; see Display Rules.
Remove Fields from Form
Request URL
DELETE https://api.epsilon3.io/v1/forms-fields
Example Request Body
{
"form_id": "3f7b9c1d-2e4a-4b6c-8d9e-0f1a2b3c4d5e",
"field_ids": ["fld_sys_5"]
}
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/forms-fields',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={ "form_id": "3f7b9c1d-2e4a-4b6c-8d9e-0f1a2b3c4d5e", "field_ids": ["fld_sys_5"] }
)
curl -X DELETE \
https://api.epsilon3.io/v1/forms-fields \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{ "form_id": "3f7b9c1d-2e4a-4b6c-8d9e-0f1a2b3c4d5e", "field_ids": ["fld_sys_5"] }'
$apiUrl = "https://api.epsilon3.io/v1/forms-fields"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
form_id = "3f7b9c1d-2e4a-4b6c-8d9e-0f1a2b3c4d5e"
field_ids = @("fld_sys_5")
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers $headers -Body $body
DELETE https://api.epsilon3.io/v1/forms-fields
Detach one or more fields from a form. The custom field definitions are not deleted: they remain in List Custom Fields and can be attached again.
Request Body
form_id: (required)idof one of your workspace's forms. Must be a UUID.field_ids: (required) array of 1–100 custom field definition ids to detach. Ids that are not attached to the form are ignored.
Response
The response status code will be 204 with no body. Returns 404 with { "message": "ID '<form_id>' Not Found" } when form_id is not one of your workspace's forms.
Returns 400 with message set to Cannot delete fields that are used in rules when a field being removed is the trigger of another field's display rule on the form, even if that other field is in the same request. Remove or change that rule first, or detach the field that owns the rule in a separate request before detaching its trigger. Fields referenced by a computed field's expression are rejected the same way with Cannot delete fields that are referenced in computed expressions.
Display Rules
Display Rule
{
"action": "SHOW" | "HIDE",
"expression": {
"field_system_id": string,
"operator": string,
"operand": <operator-dependent — see below>
}
}
Example: show a Text field only when a Custom List field has a matching selection
{
"action": "SHOW",
"expression": {
"field_system_id": "fld_sys_4",
"operator": "INCLUDES",
"operand": ["Anodized", "Painted"]
}
}
Example: hide a field while a Number field is out of range
{
"action": "HIDE",
"expression": {
"field_system_id": "fld_sys_6",
"operator": "RANGE",
"operand": { "min": 0, "max": 100, "include_min": true, "include_max": true }
}
}
Example: show a field once a date has been entered
{
"action": "SHOW",
"expression": {
"field_system_id": "fld_sys_7",
"operator": "IS_NOT_EMPTY"
}
}
A display rule shows or hides a field on a form based on the value of another field on the same form. Set it with rule on Update Form Field; it is returned as rule on each field in List Forms and Retrieve Form.
action:SHOWdisplays the field only whileexpressionis true;HIDEhides the field whileexpressionis true.- A
rulewithoutaction,expression, orexpression.operator, or with anactionother thanSHOWorHIDE, is rejected with 422 before the expression is checked. expression.field_system_id: theidof the trigger field's definition. The trigger must be attached to the same form. Returns 400 withmessageset toInvalid field_id in rule expression: <id>otherwise.expression.operator: one of the operators for the trigger field's type, below.expression.operand: the value the trigger field is compared against, in the shape listed for the operator. Omitted forIS_EMPTYandIS_NOT_EMPTY. The operand is stored as sent, so an operand of the wrong shape is accepted but the rule never matches.
Form responses also include the trigger field's field_id and field_name inside expression. They are ignored when writing a rule.
| Trigger field type | operator |
operand |
|---|---|---|
| Any | IS_EMPTY, IS_NOT_EMPTY |
none |
Text |
EQUALS, CONTAINS, DOES_NOT_CONTAIN, STARTS_WITH, ENDS_WITH |
string |
Number |
EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL |
number |
Number |
RANGE |
{ "min": number, "max": number, "include_min": boolean, "include_max": boolean } |
List, Custom List |
INCLUDES, DOES_NOT_INCLUDE |
array of option display texts |
External Data |
INCLUDES, DOES_NOT_INCLUDE |
array of external data item ids |
Date, Time, Date Time |
AFTER, BEFORE |
{ "date": string, "time": string, "zone": string } (any field may be omitted), or { "dayOffset": number } for a day relative to today |
Date, Time, Date Time |
DATETIME_RANGE |
{ "start": timestamp, "end": timestamp, "include_start": boolean, "include_end": boolean } where each timestamp is { "date", "time", "zone" } |
Part form trigger
Example: show an Inventory Item field only for items whose part uses one of two Part forms
{
"action": "SHOW",
"expression": {
"field_system_id": "part_form",
"operator": "INCLUDES",
"operand": ["6f1d2c3b-4a5e-4f60-8a9b-0c1d2e3f4a5b", "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"]
}
}
On InventoryItem forms, a rule can trigger on the form of the item's part instead of on a field value. Use the literal part_form as field_system_id. The operand is an array of Part form ids, from GET /v1/forms?entity=Part. This lets an item field appear only for items of parts configured with a given Part form.
- Valid only on forms whose
entity_typeisInventoryItem. On any other form it is rejected withInvalid field_id in rule expression: part_form. operatormust beINCLUDESorDOES_NOT_INCLUDE. Any other operator is rejected withUnsupported operator for part form trigger: <operator>.operandmust be a non-empty array of existing Part form ids, archived forms included. An unknown id, or a form of another entity type, is rejected withInvalid part form id in rule expression: <id>; an empty or missing array withPart form trigger operand must be a non-empty array of form ids.- All of these return 400 with the message in
message.
In form responses the trigger is returned with field_system_id and field_id set to part_form and field_name set to Part Form. The rule is applied wherever the item's fields are shown, including item detail and edit pages and view_inventory_details blocks in procedures and runs.
Recorded Custom Field Values
When a custom field is attached to an entity (e.g., a procedure or run), the recorded value is returned as a CustomFieldValue object. This shape is referenced by other endpoints — for example, details and procedure_details on runs and procedures.
CustomFieldValue
{
"id": string,
"field_id": string,
"name": string,
"type": "Text" | "Number" | "Date" | "Time" | "Date Time" | "Checkbox" | "List" | "Custom List" | "External Data",
"metadata": (optional) object,
"value": (optional) {
"recorded": <type-dependent — see below>
}
}
id: opaque identifier of this value instance (distinct from the custom field definitionid).field_id: stable identifier of the custom field definition this value belongs to. Use this when constructing filter query parameters such asdetail_{field_id}.name: human-readable name copied from the custom field definition at the time the value was captured.type: one of the field types listed above. Mirrors thetypereturned by the list custom fields endpoint.metadata: present for field types whose definition carries metadata (Number→{ units },List→{ list_id },Custom List→{ options },External Data→{ external_data_type, dictionary_id }).value: omitted when there is no recorded value at all. When present,value.recordedmay still benullfor fields that have been added to the procedure's header but not yet filled in.
The shape of value.recorded depends on the field type:
type |
value.recorded shape |
|---|---|
Text |
string or null |
Custom List |
string, or an array of string when the field allows multiple selections, or null |
List |
{ "value": string, "option_id": (optional) string, "numberValue": (optional) number }, or string for values recorded before option ids were captured, or an array of those objects when the field allows multiple selections, or null |
Number |
number (or numeric string) |
Checkbox |
boolean |
Date, Time, Date Time |
{ "date": string, "time": string, "zone": string } (any field may be omitted) |
External Data |
{ "id": string, "name": string, "label": (optional) string } |
Example: scalar values
[
{
"id": "valFv1k2",
"field_id": "priority",
"name": "Priority",
"type": "Custom List",
"metadata": { "options": ["High", "Medium", "Low"] },
"value": { "recorded": "High" }
},
{
"id": "val8mQp3",
"field_id": "mass",
"name": "Mass",
"type": "Number",
"metadata": { "units": "kg" },
"value": { "recorded": 12.5 }
},
{
"id": "valCh3kx",
"field_id": "is_critical",
"name": "Is Critical",
"type": "Checkbox",
"value": { "recorded": true }
}
]
Example: predefined list value (
List)
[
{
"id": "valLs7q4",
"field_id": "disposition",
"name": "Disposition",
"type": "List",
"metadata": { "list_id": "ls_disposition" },
"value": {
"recorded": {
"value": "Use As Is",
"option_id": "lso_9Kd2mQ"
}
}
},
{
"id": "valLs7q5",
"field_id": "risk_level",
"name": "Risk Level",
"type": "List",
"metadata": { "list_id": "ls_risk" },
"value": {
"recorded": {
"value": "High",
"option_id": "lso_4Tp8nR",
"numberValue": 3
}
}
}
]
Example: timestamp value (
Date,Time,Date Time)
{
"id": "valDt9wp",
"field_id": "due_at",
"name": "Due At",
"type": "Date Time",
"value": {
"recorded": {
"date": "2026-05-18",
"time": "14:30:00",
"zone": "America/Los_Angeles"
}
}
}
Example: unfilled field on a run
{
"id": "valNullx",
"field_id": "operator_notes",
"name": "Operator Notes",
"type": "Text",
"value": { "recorded": null }
}
Predefined Lists
A Predefined List is a named, reusable set of options managed at the workspace level. Custom fields
of type List do not store their own options — they reference a Predefined List by its id through
metadata.list_id, and every field pointing at that list shows the same values.
This means the endpoints below, not the custom field endpoints, are where
you add, rename, or retire the values a List field offers. Fields of type Custom List are the
opposite case: they own their options outright and are edited through
PATCH /v1/fields/:id.
List Predefined Lists
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/lists',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/lists \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/lists"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/lists
Retrieve all Predefined Lists in your workspace. Use this to find the id to put in a List
field's metadata.list_id, or to read a list's current options before updating them.
Example Response Body
[
{
"id": "ls_51bGeAUzlRgogkybghv1wX",
"name": "My First List",
"options": [
{ "id": "lso_LrBA2KtbCybbm8IASbQBUI", "display": "Option 1" },
{ "id": "lso_wmOxSdfNgJT0YR6iqRRu6o", "display": "Option 2" }
],
"hide_numeric_values": false,
"archived": false
}
]
Response
The response status code will be 200 and return a bare JSON array — not a { "data": [...] }
envelope. Archived lists are included, distinguished by "archived": true.
Each list object contains:
| Field | Type | Description |
|---|---|---|
id |
string | Unique identifier for the list, referenced by a List field's metadata.list_id |
name |
string | Display name of the list |
options |
array | The selectable options — see Option objects |
hide_numeric_values |
boolean | When true, option values are still available to expressions but hidden from operators in runs |
archived |
boolean | Whether the list has been retired |
Option objects
Every option is returned as an object. On input you may send either an option object or a bare
string, which is shorthand for { "display": "<string>" }.
| Field | Type | Description |
|---|---|---|
display |
string | (required) The text shown to operators, 1–255 characters |
id |
string | Stable identifier. Generated by the server when you omit it — see the warning below |
value |
number | (optional) Numeric value for the option, usable in expressions |
description |
string | (optional) Longer explanation, up to 1000 characters |
archived |
boolean | (optional) Whether the individual option has been retired |
group |
string | (optional) Group heading for the option, up to 255 characters |
Option ids must be unique within a list. A list may hold up to 50000 options.
Create Predefined Lists
Example Request Body
{
"lists": [
{
"id": "issue_status",
"name": "Issue Status",
"options": ["Open", "In Progress", "Closed"]
}
]
}
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/lists',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"lists": [
{ "id": "issue_status", "name": "Issue Status", "options": ["Open", "In Progress", "Closed"] }
]
}
)
curl -X POST \
https://api.epsilon3.io/v1/lists \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"lists": [
{ "id": "issue_status", "name": "Issue Status", "options": ["Open", "In Progress", "Closed"] }
]
}'
$apiUrl = "https://api.epsilon3.io/v1/lists"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
lists = @(
@{ id = "issue_status"; name = "Issue Status"; options = @("Open","In Progress","Closed") }
)
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
POST https://api.epsilon3.io/v1/lists
Add one or more Predefined Lists to your workspace.
Request Body
lists: an array (1–100) of objects, each with the following fields
id: (required) string, chosen by you and unique across all Predefined Lists in the workspacename: (required) string, 1–255 charactersoptions: (required) array of up to 50000 options; may be emptyhide_numeric_values: (optional) boolean, defaults tofalse
Example Response Body
{
"ids": ["issue_status"]
}
Response
The response status code will be 201 and return { "ids": [...] } — the ids of the lists created.
Reusing an existing list id returns a 409.
Update a Predefined List
Example Request Body
{
"options": [
{ "id": "lso_LrBA2KtbCybbm8IASbQBUI", "display": "Option 1" },
{ "id": "lso_wmOxSdfNgJT0YR6iqRRu6o", "display": "Option 2" },
"Option 3"
]
}
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v1/lists/ls_51bGeAUzlRgogkybghv1wX',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"options": [
{ "id": "lso_LrBA2KtbCybbm8IASbQBUI", "display": "Option 1" },
{ "id": "lso_wmOxSdfNgJT0YR6iqRRu6o", "display": "Option 2" },
"Option 3"
]
}
)
curl -X PATCH \
https://api.epsilon3.io/v1/lists/ls_51bGeAUzlRgogkybghv1wX \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"options": [
{ "id": "lso_LrBA2KtbCybbm8IASbQBUI", "display": "Option 1" },
{ "id": "lso_wmOxSdfNgJT0YR6iqRRu6o", "display": "Option 2" },
"Option 3"
]
}'
$apiUrl = "https://api.epsilon3.io/v1/lists/ls_51bGeAUzlRgogkybghv1wX"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
options = @(
@{ id = "lso_LrBA2KtbCybbm8IASbQBUI"; display = "Option 1" },
@{ id = "lso_wmOxSdfNgJT0YR6iqRRu6o"; display = "Option 2" },
"Option 3"
)
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
PATCH https://api.epsilon3.io/v1/lists/:listId
Update a Predefined List. This is how you add, rename, reorder, or retire the values offered by
every List custom field pointing at this list. Every parameter is optional; omitted parameters are
left untouched.
Request Body
name: (optional) string, 1–255 charactersoptions: (optional) array of up to 50000 options; may be emptyhide_numeric_values: (optional) booleanarchived: (optional) boolean
Response
The response status code will be 204 with an empty body. Fetch
GET /v1/lists to confirm the result. Updating a list that does not exist
returns a 404, and malformed options return a 422.
There is no delete endpoint. To retire a list, archive it with { "archived": true }. Archived
lists are still returned by GET /v1/lists.
Dictionary API
Dictionaries are used to organize your commands, telemetry parameters, and external data types. Epsilon3’s dictionary API supports creating, updating, retrieving, or deleting dictionary definitions. Creating a dictionary with the API allows the user to link commands, telemetry, and external data to that dictionary which enables the following in Epsilon3:
- Limit telemetry, commanding, and external data fields to specific dictionaries during procedure edit
- Switch dictionaries in procedures and retain commands that have the same name
- Switch dictionaries when starting runs using dictionary groups
Create a dictionary
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/dictionaries',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"name": "Vehicle-1"
}
)
curl -X POST \
https://api.epsilon3.io/v1/dictionaries \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"name": "Vehicle-1"
}'
$apiUrl = "https://api.epsilon3.io/v1/dictionaries"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"name": "Vehicle-1"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body (Dictionary Object)
{
"name": string,
"dictionary_group_ids": array (integer) (optional)
}
POST https://api.epsilon3.io/v1/dictionaries
The request body should contain a Dictionary object. The field name is required. The name field should be unique.
Response Body
{
"id": integer,
}
List all available dictionaries
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/dictionaries',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/dictionaries \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/dictionaries"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/dictionaries
Sample Request with Query Parameters
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/dictionaries',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
params={
"dictionary-group-ids": "1,5,28"
}
)
curl -X GET \
"https://api.epsilon3.io/v1/dictionaries?dictionary-group-ids=1,5,28" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/dictionaries?dictionary-group-ids=1,5,28"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/dictionaries?dictionary-group-ids={dictionary-group-ids}
The optional URL parameter dictionary-group-ids may be provided to filter dictionaries by dictionary groups. The value should be a comma-separated list of dictionary group IDs. The response will only contain dictionaries that are members of at least one of the specified dictionary groups.
Response Body
[{
"id": integer,
"name": string,
"created_at": string, timestamp in ISO-8601 format,
"groups: [{
"id": integer,
"name": string,
"created_at": string, timestamp in ISO-8601 format
}]
}]
Update a dictionary
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v1/dictionaries/2',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"dictionary_group_ids": [1, 2, 3]
}
)
curl -X PATCH \
https://api.epsilon3.io/v1/dictionaries/2 \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"dictionary_group_ids": [1, 2, 3]
}'
$apiUrl = "https://api.epsilon3.io/v1/dictionaries/2"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"dictionary_group_ids": @(1, 2, 3)
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
Request Body
{
"dictionary_group_ids": array (integer)
}
PATCH https://api.epsilon3.io/v1/dictionaries/:dictionaryId
The request body should contain a list of dictionary group ids.
Delete dictionaries
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/dictionaries',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"ids": [1, 2, 3]
}
)
curl -X DELETE \
https://api.epsilon3.io/v1/dictionaries \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"ids": [1, 2, 3]
}'
$apiUrl = "https://api.epsilon3.io/v1/dictionaries"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"ids": @(1, 2, 3)
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers $headers -Body $body
DELETE https://api.epsilon3.io/v1/dictionaries
Request Body
{
"ids": array (integer),
}
Example Request Body
{
"ids": [2, 3, 10]
}
`
Dictionary Group API
Dictionary groups are used to group dictionaries and their associated commands and telemetry parameters. Epsilon3’s dictionary groups API supports creating, retrieving, or deleting dictionary groups. Creating a dictionary group with the API allows the user to link dictionaries to that group which enables the following in Epsilon3:
- Limit telemetry and commanding to specific dictionary groups during procedure edit
- Set the dictionary of commands and telemetry parameters when starting runs
Dictionary groups are useful for cases where multiple dictionaries have telemetry parameters or commands with the same name. The dictionary group will contain the telemetry parameters and commands whose names are present in all dictionaries in the group.
Create a dictionary group
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/dictionary-groups',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"name": "Vehicles"
}
)
curl -X POST \
https://api.epsilon3.io/v1/dictionary-groups \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"name": "Vehicles"
}'
$apiUrl = "https://api.epsilon3.io/v1/dictionary-groups"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"name": "Vehicles"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body (Dictionary Group Object)
{
"name": string
}
POST https://api.epsilon3.io/v1/dictionary-groups
The request body should contain a Dictionary Group object. The field name is required. The name field should be unique.
Response Body
{
"id": integer,
}
List all available dictionary groups
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/dictionary-groups',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/dictionary-groups \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/dictionary-groups"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/dictionary-groups
Response Body
[{
"id": integer,
"name": string,
"created_at": string, timestamp in ISO-8601 format
}]
Delete dictionary groups
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/dictionary-groups',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"ids": [1, 2, 3]
}
)
curl -X DELETE \
https://api.epsilon3.io/v1/dictionary-groups \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"ids": [1, 2, 3]
}'
$apiUrl = "https://api.epsilon3.io/v1/dictionary-groups"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"ids": @(1, 2, 3)
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers $headers -Body $body
DELETE https://api.epsilon3.io/v1/dictionary-groups
Request Body
{
"ids": array (integer),
}
Example Request Body
{
"ids": [2, 3, 10]
}
`
Events API
Events are used in schedules to plan operations and milestones. Epsilon3's events API currently supports the following capabilities.
Get Events
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/planning/events',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
params={
"start": "2023-07-26T00:00:00.000Z",
"end": "2023-08-09T00:00:00.000Z",
"eventType": "unscheduled",
"exclude_recurrences": True,
}
)
curl -X GET \
"https://api.epsilon3.io/v1/planning/events?start=2023-07-26T00:00:00.000Z&end=2023-08-09T00:00:00.000Z&eventType=unscheduled&exclude_recurrences=true" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/planning/events?start=2023-07-26T00:00:00.000Z&end=2023-08-09T00:00:00.000Z&eventType=unscheduled&exclude_recurrences=true"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET /api/v1/planning/events
Retrieves events under
Query Parameters
- start (required unless exclude_recurrences or eventType is 'unscheduled'): The start of the time range to fetch events from. Should be in ISO string format.
- end (required unless exclude_recurrences or eventType is 'unscheduled'): The end of the time range to fetch events from. Should be in ISO string format.
- eventType (optional): To get 'unscheduled' events, pass this as a query parameter. If provided with 'unscheduled', start and end parameters become optional.
- exclude_recurrences (optional): To get only non-recurring events, pass this as a query parameter with value true. If provided, start and end parameters become optional.
Request URL
GET /api/v1/planning/events?start=2023-07-26T00:00:00.000Z&end=2023-08-09T00:00:00.000Z
Response Body
{
"events": [
{
"id": "DVEYVZ2qp8sk2KgvA6zOkn",
"name": "Galactic Innovators Summit",
"start": "2023-08-30T13:00:00.000Z",
"end": "2023-08-30T18:00:00.000Z",
"procedure_id": null,
"procedure_section_id": null,
"run_id": null,
"swimlane_id": null,
"description": null,
"duration": "PT5H"
},
{
"id": "DphgSy369ewI3QbzIzhjz6",
"name": "Space Exploration Workshop",
"start": "2023-08-30T10:30:00.000Z",
"end": "2023-08-30T12:30:00.000Z",
"procedure_id": null,
"procedure_section_id": null,
"run_id": null,
"swimlane_id": null,
"description": null,
"duration": "PT2H"
}
]
}
Get Event
GET /api/v1/planning/events/:eventId
Retrieves a specific event.
Parameters
eventId(path parameter): Id of the event.
Request URL
GET /api/v1/planning/events/test_event_id
Response Body
{
"id": "QuACGmyZHp7qMMOUM64SRg",
"name": "Space Colony Simulation Test",
"description": "Test Description",
"start": "2023-07-30T09:00:00.000Z",
"end": "2023-07-30T17:00:00.000Z",
"duration": "PT8H",
"procedure_id": "45KqIG4QZV1S5oOYZaxE9y",
"procedure_section_id": null,
"run_id": null,
"swimlane_id": "DpSTO81RP9fZSUd8d8ffY4",
"rrule": "RRULE:FREQ=DAILY;INTERVAL=3",
"recurrence_id": "ChEIi3cXJVXCidYlVgMqTl",
"recurrence_date": null,
"notes": "Here are some notes with:\n- **Markdown components**",
"dependencies": {
"in": [
{
"id": "DhawCJ3URB7sNd8rRZ8r3H",
"source": "1RXszDst3d16WeXn1GiNLB"
}
],
"out": [
{
"id": "jX88nSfgTK22KL7DZyUJjc",
"target": "3rfiluxsbzKwGbJOW8VIAZ"
}
]
},
"status": null,
"predecessor_id": null
}
If the event doesn't exist, it will respond with a 404 status code.
Update Event
Request URL
PATCH /api/v1/planning/events/test_event_id
Example Request Body
{
"id": "3rfiluxsbzKwGbJOW8VIAZ",
"name": "Test Event",
"description": "Test Description",
"start": "2023-07-26T17:00:00Z",
"duration": "PT6H4S",
"dependencies": {
"in": [
{
"source": "QuACGmyZHp7qMMOUM64SRg",
"target": "3rfiluxsbzKwGbJOW8VIAZ"
}
],
"out": []
}
}
PATCH /api/v1/planning/events/:eventId
Updates a specific event.
Parameters
eventId(path parameter): Id of the event.
Request Body
The request body should be a JSON object that contains the details of the event to be updated. The following fields are supported:
name: (required) Event name, must be a string.start: (optional) Start time of the event, must be an ISO datetime string.duration: (optional) Duration of event, must be in ISO 8601 duration string. This is used to calculate event's end date.procedure_id: (optional) Procedure id.procedure_section_id: (optional) Procedure section id.run_id: (optional) Run id.swimlane_id: (optional) Swimlane id.dependencies: (optional) Dependencies of the event, must be an object.dependencies.in: (optional) Incoming dependencies, must be an array.dependencies.out: (optional) Outgoing dependencies, must be an array.
rrule: (optional) Recurrence rule.recurrence_id: (optional) Recurrence id.description: (optional) Event description.notes: (optional) Event notes.status: (optional) Event status can be one of these following values: 'completed' | 'running' | 'paused' | 'success' | 'abort' | 'failure' | 'planning' | 'ended';milestone: (optional)
Response The response status code will be 200 and return a copy of the updated event.
Create Event
POST /api/v1/planning/events
Request URL
POST /api/v1/events
Example Request Body
{
"name": "Test Event",
"description": "Description goes here",
"start": "2023-08-08T00:00:00.000Z",
"duration": "PT6H4S",
"procedure_id": "45KqIG4QZV1S5oOYZaxE9y",
"procedure_section_id": null,
"dependencies": { "in": [{ "source": "6NSi9FIBDN1xrWEV4Q8HQR" }], "out": [] },
"swimlane_id": "DpSTO81RP9fZSUd8d8ffY4",
"rrule": "RRULE:FREQ=DAILY;INTERVAL=1",
"notes": "Here are some notes with **markdown**"
}
Creates a new event.
Parameters
No path parameters are required for this endpoint.
Request Body
The request body should be a JSON object that contains the details of the event to be created. The following fields are supported:
name: (required) Event name, must be a string.start: (optional) Start time of the event, must be an ISO datetime string.duration: (optional) Duration of event, must be in ISO 8601 duration string. This is used to calculate event's end date.procedure_id: (optional) Procedure id.procedure_section_id: (optional) Procedure section id.swimlane_id: (optional) Swimlane id.dependencies: (optional) Dependencies of the event, must be an object.dependencies.in: (optional) Incoming dependencies, must be an array.dependencies.out: (optional) Outgoing dependencies, must be an array.
rrule: (optional) Recurrence rule.description: (optional) Event description.notes: (optional) Event notes.status: (optional) Event status can be one of these following values: 'completed' | 'running' | 'paused' | 'success' | 'abort' | 'failure' | 'planning' | 'ended';milestone: (optional)
Response
The response status code will be 201 and return a JSON object containing the ID of the newly created event.
Delete Event
DELETE /api/v1/planning/events/:eventId
Request URL
DELETE /api/v1/planning/events/test_event_id
Deletes an event.
Parameters
eventId(path parameter): Id of the event.
Response
The response status code will be 204 for a successful response.
External Data API
Epsilon3’s external data API supports the following capabilities:
- Create an external data type that can be selected from a dropdown in procedures
- Update an existing external data type
- Connect an external data service that can be queried in procedures
Create an external data item type
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/external-data',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"type": "x-axis",
"label": "X Axis",
"dictionary_id": 1
}
)
curl -X POST \
https://api.epsilon3.io/v1/external-data \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"type": "x-axis",
"label": "X Axis",
"dictionary_id": 1
}'
$apiUrl = "https://api.epsilon3.io/v1/external-data"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"type": "x-axis",
"label": "X Axis",
"dictionary_id": 1
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"type": string,
"label": string,
"dictionary_id": integer (optional)
}
POST https://api.epsilon3.io/v1/external-data
The request body should contain an External Data Item object with the following required fields: type & label. The optional dictionary_id field allows the External Data Item to be related to a dictionary. If no dictionary_id is provided, the external data item will be related to the default dictionary.
Update an external data item type
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v1/external-data/1',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"label": "Updated Label"
}
)
curl -X PATCH \
https://api.epsilon3.io/v1/external-data/1 \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"label": "Updated Label"
}'
$apiUrl = "https://api.epsilon3.io/v1/external-data/1"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"label" = "Updated Label"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
Request Body
{
"type": string (optional),
"label": string (optional),
"dictionary_id": integer (optional)
}
PATCH https://api.epsilon3.io/v1/external-data/:id
Updates an existing External Data Item type. All body fields are optional — only the fields provided will be updated. The :id path parameter is required and must be the integer ID of the external data item to update.
List all external data items
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/external-data',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/external-data \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/external-data"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/external-data
Example Response
[
{
"type": "work_order",
"label": "Work Order",
"dictionary_id": 1
},
{
"type": "part_number",
"label": "Part Number",
"dictionary_id": 1
},
{
"type": "support_ticket",
"label": "Support Ticket",
"dictionary_id": 1
}
]
Delete external data
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/external-data',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"ids": [1, 2, 3]
}
)
curl -X DELETE \
https://api.epsilon3.io/v1/external-data \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"ids": [1, 2, 3]
}'
$apiUrl = "https://api.epsilon3.io/v1/external-data"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"ids": @(1, 2, 3)
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers $headers -Body $body
DELETE `https://api.epsilon3.io/v1/external-data
Request Body
{
"ids": array (integer),
}
Example Request Body
{
"ids": [123, 456]
}
Files API
Support retrieving Files.
Retrieve a file
This endpoint retrieves a single file.
Sample Request
import requests
import os
import mimetypes
response = requests.get(
url='https://api.epsilon3.io/v1/files/sample-file-id',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
stream=True,
)
content_type = response.headers.get("Content-Type", "")
extension = mimetypes.guess_extension(content_type.split(";")[0].strip())
with open("image_name_here" + extension, "wb") as file:
for chunk in response.iter_content(chunk_size=8192):
file.write(chunk)
# Capture headers into a variable
headers=$(curl -u "$EPSILON3_API_KEY:" -D - "https://api.epsilon3.io/v1/files/sample-file-id" -o temp_file)
# Extract Content-Type
content_type=$(echo "$headers" | grep -i '^Content-Type:' | cut -d' ' -f2 | tr -d '\r\n')
# Determine extension
case "$content_type" in
image/png) ext=".png" ;;
image/jpeg) ext=".jpg" ;;
image/gif) ext=".gif" ;;
application/pdf) ext=".pdf" ;;
*) ext=".bin" ;;
esac
# Rename the file to add extension
mv temp_file "your_downloaded_file_name$ext"
$apiUrl = "https://api.epsilon3.io/v1/files/sample-file-id"
$apiKey = $env:EPSILON3_API_KEY
# Set up basic auth
$headers = @{
Authorization = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
}
# Download file and capture headers
$response = Invoke-RestMethod -Uri $Url -Headers $headers -OutFile "temp_file" -PassThru
# Parse content-type
$contentType = $response.Headers["Content-Type"].Split(";")[0].Trim()
# Determine extension
switch ($contentType) {
"image/png" { $ext = ".png" }
"image/jpeg" { $ext = ".jpg" }
"image/gif" { $ext = ".gif" }
"application/pdf" { $ext = ".pdf" }
default { $ext = ".bin" }
}
# Rename to add extension
Rename-Item -Path "temp_file" -NewName ("your_downloaded_file_name" + $ext)
GET https://api.epsilon3.io/v1/files/sample-file-id
Issues API
Support managing Issues though their entire lifecycle.
Create an issue
This endpoint creates a new issue.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/issues',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"title": "Sample title",
"notes": "Some notes",
"run_id": "up9goX1AgzCuffxUXbpI1K",
"severity_id": "123",
"status_id": "123",
"assignee": "test@epsilon3.io",
"project_id": "h78svHgfi4",
"reference_id": "bIkugfdu94hG42jWkfbh2",
"reference_type": "run",
"details": [{
"id": "10",
"value": { "recorded": "Sample value"}
}]
}
)
curl -X POST \
https://api.epsilon3.io/v1/issues \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"title": "Sample title",
"notes": "Some notes",
"run_id": "up9goX1AgzCuffxUXbpI1K",
"severity_id": "123",
"status_id": "123",
"assignee": "test@epsilon3.io",
"project_id": "h78svHgfi4",
"reference_id": "bIkugfdu94hG42jWkfbh2",
"reference_type": "run",
"details": [{
"id": "10",
"value": { "recorded": "Sample value"}
}]
}'
$apiUrl = "https://api.epsilon3.io/v1/issues"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"title": "Sample title",
"notes": "Some notes",
"run_id": "up9goX1AgzCuffxUXbpI1K",
"severity_id": "123",
"status_id": "123",
"assignee": "test@epsilon3.io",
"project_id": "h78svHgfi4",
"reference_id": "bIkugfdu94hG42jWkfbh2",
"reference_type": "run",
"details": [{
"id": "10",
"value": { "recorded": "Sample value"}
}]
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"title": string,
"notes": (optional) string,
"run_id": (optional) string,
"severity_id": (optional) string,
"status_id": (optional) string,
"assignee": (optional) string,
"project_id": string | null,
"reference_id": (optional) string,
"reference_type": (optional) string,
"details": (optional) [{
"id": string,
"value": { "recorded": string }
}]
}
POST https://api.epsilon3.io/v1/issues
Example Response Body
{
"id": 123,
"display_id": "NCR-001"
}
display_id is the issue's display label, composed from its issue type's code
prefix and its per-type number. It is a label, not an identifier — it changes if
the issue type's code is renamed, so use id for anything durable. It is omitted
when the team creates issues through a Jira integration.
Update an issue
This endpoint updates an issue.
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v1/issues/123',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"title": "Sample title",
"notes": "Some notes",
"run_id": "up9goX1AgzCuffxUXbpI1K",
"severity_id": "123",
"status_id": "123",
"assignee": "test@epsilon3.io",
"project_id": "h78svHgfi4",
"reference_id": "bIkugfdu94hG42jWkfbh2",
"reference_type": "run",
"details": [{
"id": "10",
"value": { "recorded": "Sample value"}
}]
}
)
curl -X PATCH \
https://api.epsilon3.io/v1/issues/123 \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"title": "Sample title",
"notes": "Some notes",
"run_id": "up9goX1AgzCuffxUXbpI1K",
"severity_id": "123",
"status_id": "123",
"assignee": "test@epsilon3.io",
"project_id": "h78svHgfi4",
"reference_id": "bIkugfdu94hG42jWkfbh2",
"reference_type": "run",
"details": [{
"id": "10",
"value": { "recorded": "Sample value"}
}]
}'
$apiUrl = "https://api.epsilon3.io/v1/issues/123"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"title": "Sample title",
"notes": "Some notes",
"run_id": "up9goX1AgzCuffxUXbpI1K",
"severity_id": "123",
"status_id": "123",
"assignee": "test@epsilon3.io",
"project_id": "h78svHgfi4",
"reference_id": "bIkugfdu94hG42jWkfbh2",
"reference_type": "run",
"details": [{
"id": "10",
"value": { "recorded": "Sample value"}
}]
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
Request Body
{
"title": (optional) string,
"notes": (optional) string,
"run_id": (optional) string,
"severity_id": (optional) string,
"status_id": (optional) string,
"assignee": (optional) string,
"project_id": string | null,
"details": (optional) [{
"id": string,
"value": { "recorded": string }
}]
steps: [{
"id": string,
"name": string,
"signoffs": [{
"id": string,
"operators": Array<string>,
}]
"content": [{
"id": string,
"type": 'alert',
"subtype": 'note' | 'caution' | 'warning',
"text": string,
} | {
"id": string,
"type": 'procedure_link',
"procedure": string,
"section": string,
"run": (optional) string,
}]
}];
}
PATCH https://api.epsilon3.io/v1/issues/123
Attach files to an issue
Sample Request
import requests
import os
with open('traveler.pdf', 'rb') as traveler, open('torque.png', 'rb') as torque:
requests.post(
url='https://api.epsilon3.io/v1/issues/42/files',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
files=[('file', traveler), ('file', torque)]
)
curl -X POST \
https://api.epsilon3.io/v1/issues/42/files \
-u "$EPSILON3_API_KEY": \
-F "file=@traveler.pdf" \
-F "file=@torque.png"
$apiUrl = "https://api.epsilon3.io/v1/issues/42/files"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$form = @{
file = @(Get-Item -Path "traveler.pdf"; Get-Item -Path "torque.png")
}
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
} -Form $form
POST https://api.epsilon3.io/v1/issues/:issueId/files
Uploads up to 10 files and adds them to the attachments of the specified issue. Existing attachments are kept. Either every file is attached or none is. Attached files are returned in the attachments array of the issue. Returns 409 while the issue's current stage is in review, and 422 when the issue is archived.
Parameters
issueId(path parameter): ID of the issue, or its display ID such asNCR-001.
Example Response Body
{
"attachments": [
{
"attachment_id": "f48c1df3-dc39-4a1d-90aa-30205f9e8653",
"name": "traveler.pdf",
"content_type": "application/pdf"
},
{
"attachment_id": "0b8d6c1e-6a0f-4d43-9f53-4d1e8a2f7c11",
"name": "torque.png",
"content_type": "image/png"
}
]
}
Request Body
This request should be sent as multipart/form-data containing the following fields:
file: (required) A file to attach. Repeat the field to attach up to 10 files in one request. Each file must not exceed500 MB.attachment_id: (optional) A UUID to store the file under. Send it once per file, in the same order as the files. When omitted, an ID is generated.name: (optional) The attachment name, up to 255 characters. Send it once per file, in the same order as the files. When omitted, the name of the uploaded file is used.
The content type, size and source of each attachment are set from the uploaded file. Sending attachment_id or name a different number of times than file, or an attachment_id that is already in use, returns 422 Unprocessable Entity.
Response
- Status:
201 Created - Body: object with the stored attachments, in upload order:
attachments: Array of attachments, each with:attachment_id: ID of the stored filename: Name of the attachmentcontent_type: MIME type of the file
Retrieve single issue
This endpoint allows you to retrieve a single issue with all of its associated data.
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/issues/123',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
)
curl -X GET \
"https://api.epsilon3.io/v1/issues/123" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/issues/123"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/issues/:issue-id
Example Response Body
{
title: string,
notes?: string,
created_by: string,
code?: string | null,
issue_type_id?: string | null,
run_id?: string,
reference_id: string,
reference_type: 'step' | 'section' | 'run' | 'item' | 'part' | 'order' | 'tool' | 'tool_instance' | 'risk' | 'work_order' | 'issue' | 'location' | 'unlinked',
severity: {
"id": number,
"name": string,
"color": string,
"pausesRun": boolean,
};
status: {
"id": number,
"status": draft | active | closed,
"name": string,
"color": string,
};
sub_status: null | {
"id": number,
"status_id": number,
"value": string,
"label": string,
"color": string,
},
attachments?: [{
"attachment_id": string,
"name": string,
"content_type": string,
}],
assignees?: Array<string>,
activity?: Array<Activity | RunActions>,
details?: [{
"id"
"value": (optional) { recorded: string | null },
}];
steps: [{
"id": string,
"name": string,
"signoffs": [{
"id": string,
"operators": Array<string>,
}]
"content": [{
"id": string,
"type": 'alert',
"subtype": 'note' | 'caution' | 'warning',
"text": string,
} | {
"id": string,
"type": 'text',
"text": string,
} | {
"id": string,
"type": 'procedure_link',
"procedure": string,
"section": string,
"run": (optional) string,
} | {
"id": string,
"type": 'attachment',
"name": (optional) string,
"attachment_id": (optional) string,
"content_type": (optional) string,
"caption": (optional) string,
} | {
"id": string,
"type": 'requirement',
"label": string,
"requirement_id": (optional) string,
} | {
"id": string,
"type": 'input',
"name": string,
"inputType": 'text' | 'number' | 'checkbox' | 'timestamp' | 'attachment' | 'list' | 'multiple_choice' | 'select' | 'scan_code' | 'sketch',
"recorded": (optional) { "value": string | number | boolean | object | null },
"actions": (optional) [{
"user_id": string,
"action_id": string,
"timestamp": string,
"recorded": { "value": string | number | boolean | object | null },
}],
} | {
"id": string,
"type": 'table_input',
"columns": Array<{ "id": string, "name": string, "column_type": 'text' | 'input', "input_type": (optional) 'text' | 'number' | 'checkbox' }>,
"rows": number,
"recorded": (optional) { "values": Array<Array<string | boolean | null>>, "version": number },
} | {
"id": string,
"type": 'field_input_table',
"fieldsPerRow": number,
"fields": Array<Input>,
}]
}];
project_id?: string;
}
Issue action steps support the same content blocks as procedure steps: alerts, text, procedure links, attachments, requirements, field inputs, table inputs, and field input tables. field_input_table.fields holds input blocks (the same shape shown above), each carrying its own recorded value.
When an operator collects data on an issue action step, the recorded value is returned inline on that content block as recorded.value (for a table_input, the collected cells are under recorded.values). For a field input, each collection is also appended to the block's actions array with the user_id, action_id, and timestamp of who recorded it and when. A field input's recorded.value is a primitive for text, number, checkbox, scan code, and select inputs, and a structured object for timestamp, attachment, sketch, and list inputs. This is how data collected on an issue is read back through the API.
Retrieve list of issues
This endpoint allows you to retrieve all available issues with basic information.
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/issues?status_types[]=draft&status_types[]=active',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
)
curl -X GET \
"https://api.epsilon3.io/v1/issues?status_types[]=draft&status_types[]=active" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/issues?status_types[]=draft&status_types[]=active"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/issues
| Query parameter | Value |
|---|---|
| run_id | String |
| status_types | Array - draft | active | closed |
| reference_id | string |
| reference_type | string |
| created_from | String - ISO 8601 date or timestamp. Only issues created at or after it |
| created_to | String - ISO 8601 date or timestamp. A date alone covers that whole day |
| maintenance_only | Boolean - Only issues raised on a run performed for a maintenance event |
| count_only | Boolean - Returns the number of matching issues instead of the issues |
| group_by | reference | reference_type | issue_type | status - Returns issue counts per group instead of the issues |
| min_count | Integer - With group_by, only groups with at least this many issues |
Query parameter example usage
GET https://api.epsilon3.io/v1/issues?status_types[]=draft&status_types[]=active&run_id=sampleRunId
count_only and group_by cannot be combined, and min_count requires group_by; either mistake returns 422. Both apply every other filter, so a count, a grouping and a listing with the same filters cover the same issues, except that min_count leaves out the groups below it.
Sample Request: count active issues per issue type created in January
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/issues?status_types[]=active&created_from=2026-01-01&created_to=2026-01-31&group_by=issue_type',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
)
curl -X GET \
"https://api.epsilon3.io/v1/issues?status_types[]=active&created_from=2026-01-01&created_to=2026-01-31&group_by=issue_type" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/issues?status_types[]=active&created_from=2026-01-01&created_to=2026-01-31&group_by=issue_type"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
Example Response Body with group_by
{
"groups": [
{ "issue_type": "NCR", "count": 12 },
{ "issue_type": "DR", "count": 3 }
]
}
Each group carries count and the keys of its group_by value:
| group_by | Keys in each group |
|---|---|
| reference | reference_type, reference_id, count |
| reference_type | reference_type, count |
| issue_type | issue_type, count |
| status | status, count |
With group_by=reference or group_by=reference_type, one issue linked to several records counts once in each of their groups, so the response also carries distinct_issues, the number of different issues across all groups.
Sample Request: count active issues raised on maintenance runs in January
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/issues?status_types[]=active&maintenance_only=true&created_from=2026-01-01&created_to=2026-01-31&count_only=true',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
)
curl -X GET \
"https://api.epsilon3.io/v1/issues?status_types[]=active&maintenance_only=true&created_from=2026-01-01&created_to=2026-01-31&count_only=true" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/issues?status_types[]=active&maintenance_only=true&created_from=2026-01-01&created_to=2026-01-31&count_only=true"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
Example Response Body with count_only
{
"count": 15
}
Example Response Body
{
"data": [
{
id: number,
title: string,
status: {
"id": number,
"status": draft | active | closed,
"name": string,
"color": string,
};
subStatus: null | {
"id": number,
"status_id": number,
"value": string,
"label": string,
"color": string,
},
created_at: string,
created_by: string,
run_id: (optional) string,
reference_id: string,
reference_type: 'step' | 'section' | 'run' | 'item' | 'part' | 'order' | 'tool' | 'tool_instance' | 'risk' | 'work_order' | 'issue' | 'location' | 'unlinked',
assignees: (optional) string,
severity?: {
"id": number,
"name": string,
"color": string,
"pausesRun": boolean,
},
projectId?: string,
}
]
}
Retrieve issues metadata options
This endpoint allows you to retrieve all options that you can set as values on an issue. These options are not customizable at this time.
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/issues/metadata-options',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
)
curl -X GET \
"https://api.epsilon3.io/v1/issues/metadata-options" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/issues/metadata-options"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/issues/metadata-options
Example Response Body
{
"status": [
{
"id": number,
"status": draft | active | closed,
"name": string,
"color": string,
}
],
"sub_status": [
{
"id": number,
"status_id": number,
"value": string,
"label": string,
"color": string,
}
],
"severity": [
{
"id": number,
"name": string,
"color": string,
"pausesRun": boolean,
}
],
}
Sign off step in an issue
This endpoint adds a signoff to resolution steps within an issue.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/issues/steps/signoff',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"step_index": 0,
"signoff_id": "so_vYY3vnQ2jr",
"operator": "",
"timestamp": "2024-10-30T20:16:58.759Z"
}
)
curl -X POST \
https://api.epsilon3.io/v1/issues/steps/signoff \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"step_index": 0,
"signoff_id": "so_vYY3vnQ2jr",
"operator": "",
"timestamp": "2024-10-30T20:16:58.759Z"
}'
$apiUrl = "https://api.epsilon3.io/v1/issues/steps/signoff"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"step_index": 0,
"signoff_id": "so_vYY3vnQ2jr",
"operator": "",
"timestamp": "2024-10-30T20:16:58.759Z"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"step_index": integer,
"signoff_id": string,
"operator": string,
"timestamp": string - ISO8601 datetime
}
POST https://api.epsilon3.io/v1/issues/steps/signoff
Procedures API
Audit procedures with invalid data
This endpoint audits procedures and returns the location of invalid telemetry, commanding or external data blocks.
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/procedures/auditInvalid',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/procedures/auditInvalid \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/auditInvalid"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/procedures/auditInvalid
Response Body
{ "procedures": [
{
"id": string,
"name": string,
"invalid_blocks": {
"section_content": [
{
"section_id": string,
"section_name": string,
"step_id": string,
"step_name": string,
"content_type": string, ( input | telemetry | commanding | external_item )
"content_name": string,
"content_id": string,
"reason": string, ( e.g. "Invalid external data" )
}
],
"procedure_variables": [
{
"variable_id": string,
"variable_name": string,
"external_search_data_type": string,
"reason": string, ( e.g. "Invalid commanding" )
}
]
}
}
]}
List procedure summaries
Retrieves a summarized view of all available procedures.
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/procedures/summary',
params={'state': 'released', 'archived': 'false'},
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
'https://api.epsilon3.io/v1/procedures/summary?state=released&archived=false' \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/procedures/summary?state=released&archived=false"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/procedures/summary
Query parameters
| Query parameter | Value |
|---|---|
state |
Filter by procedure state. One of draft, in_review, or released. |
code |
Filter by exact procedure code. |
name |
Filter by procedure name (case-insensitive substring match). |
version |
Filter by exact version string. |
owner |
Filter by exact owner user email. |
project-ids |
Filter by project ID. Accepts an array (e.g., project-ids[]=id1&project-ids[]=id2). Procedures matching any of the given IDs are returned. |
procedure-type |
Filter by procedure type. Accepts one or more of procedure, testPlan, testSequence, reference (e.g., procedure-type=testPlan). |
archived |
Filter by archived status. true or false. |
is-template |
Filter by template status. true or false. |
created-at-before |
Filter to procedures created before this ISO 8601 timestamp (exclusive). |
created-at-after |
Filter to procedures created after this ISO 8601 timestamp (exclusive). |
edited-at-before |
Filter to procedures last edited before this ISO 8601 timestamp (exclusive). |
edited-at-after |
Filter to procedures last edited after this ISO 8601 timestamp (exclusive). |
tag-ids |
Filter by global procedure tag IDs. Accepts an array (e.g., tag-ids[]=id1&tag-ids[]=id2). Procedures matching any of the given tag IDs are returned. |
source-template-id |
Filter by the template ID the procedure was created from. |
detail_{field_id} |
Filter procedures by the procedure-level custom field with the given field_id. See Custom field filters. |
All filters are optional. When multiple filters are provided they combine with AND semantics.
Query parameter example usage
GET https://api.epsilon3.io/v1/procedures/summary?state=released&owner=alice&created-at-after=2025-01-01T00:00:00Z
GET https://api.epsilon3.io/v1/procedures/summary?detail_external_id=ABC-123&detail_mass:gte=10
Custom field filters on procedures summaries
This endpoint accepts dynamic query parameters that filter results by the value of a procedure-level custom field. The parameter name encodes both the field and an optional operator.
detail_{field_id}=valuefilters by the procedure's custom field. Thefield_idis the stable identifier returned by list custom fields.- An operator suffix may be appended after a colon:
detail_{field_id}:{op}=value. When the suffix is omitted, the operator defaults toeq. - Repeating the same key creates an OR-set of accepted operands for that operator (e.g.,
detail_priority=High&detail_priority=Mediummatches procedures whosepriorityisHighorMedium). - The empty string
""is a sentinel that matches "no value recorded".
Supported operators per custom field type:
| Field type | Allowed operators |
|---|---|
Number, Date, Time, Date Time |
eq, ne, gt, gte, lt, lte |
Text |
eq, ne, contains |
List, Custom List, Checkbox |
eq, ne |
Operand format per type: ISO-8601 strings for Date / Time / Date Time, numeric strings for Number, plain strings for Text / List / Custom List, true or false for Checkbox.
contains matches Text values that include the case insensitive operand anywhere, does not accept empty strings or wildcards.
For List fields, an operand matches either the selected option's display text or its option_id (see Recorded Custom Field Values). Filtering by option_id keeps matching after an option is renamed. Values recorded without an option_id can only be matched by their text.
Response Body
{ "data": [
{
"_id": string;
"_rev": string;
"name": string;
"procedure_id": (optional) string;
"state": "draft" | "in_review" | "released";
"code": string;
"version": (optional) string;
"project_id": (optional) string;
"project_name": (optional) string;
"project_code": (optional) string;
"archived": (optional) boolean;
"editedAt": (optional) string;
"created_at": (optional) string;
"owner": (optional) string;
"procedure_type": (optional) "procedure" | "testPlan" | "testSequence" | "reference";
"is_template": (optional) boolean;
"source_template_id": (optional) string;
"template_usage_count": (optional) number;
"template_last_used_at": (optional) string;
"linked_procedure_ids": (optional) Array<string>;
"reviewer_groups": (optional) Array<ReviewerGroup>;
"start_run_signoffs_groups": (optional) Array<RunSignoffGroup>;
"tags": (optional) Array<Tag>;
"global_procedure_tag_ids": (optional) Array<string>;
"details": (optional) Array<CustomFieldValue>;
}
]}
details carries the procedure-level custom field values. The array includes an entry for every custom field added to the procedure's header, even when no value has been recorded — see Recorded Custom Field Values for the wire shape.
Retrieve a procedure
Retrieves all available information for an individual procedure by ID.
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/procedures/sampleProcedureId',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/procedures/sampleProcedureId \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/procedures/sampleProcedureId"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/procedures/:procedure-id
Query parameters
| Query parameter | Value |
|---|---|
label-style |
Optional. letters (A, A1, A1.1) or numbers (1, 1.1, 1.1.1). Overrides the workspace's Section Labels Displayed As setting for this request. Defaults to that setting. |
Response Body
{
"_id": string;
"_rev": (optional) string;
"procedure_id": (optional) string;
"project_id": (optional) string;
"project_name": (optional) string;
"project_code": (optional) string;
"code": string;
"name": string;
"variables": (optional) Array<DraftVariable>;
"headers": (optional) Array<DraftHeader>;
"version": (optional) string;
"description": string;
"archived": boolean;
"state": (optional) "draft" | "in_review";
"sections": Array<DraftSection>;
"reviewer_groups": (optional) Array<ReviewerGroup>;
"reviewer_actions_history": (optional) Array<ReviewerAction>;
"locked_by": (optional) string;
"locked_at": (optional) string;
"redline_actions": (optional) Array<RedlineAction>;
"editedAt": (optional) string;
"editedUserId": (optional) string;
"owner": (optional) string;
"procedure_rev_num": (optional) number;
"release_note": (optional) ReleaseNote;
"comments": (optional) Array<ReviewComment>;
"approvals": (optional) Array<Approval>;
"tags": (optional) Array<Tag>;
"settings": (optional) Settings;
"reviewers": (optional) Array<Reviewer>;
"end_run_signoffs_groups": (optional) Array<{
id: string;
operators: Array<string>;
}>;
"default_view_mode": (optional) string;
"dictionary_id": (optional) number;
"is_strict_signoff_enabled": (optional) boolean;
"details": (optional) Array<CustomFieldValue>;
}
details carries the procedure-level custom field values. The array includes an entry for every custom field added to the procedure's header, even when no value has been recorded — see Recorded Custom Field Values for the wire shape.
Retrieve a draft procedure
Retrieves the current draft or unreleased version of a procedure, if one exists. This works the same as Retrieve a procedure.
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/procedures/sampleProcedureId/draft',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/procedures/sampleProcedureId/draft \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/procedures/sampleProcedureId/draft"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/procedures/:procedure-id/draft
The response body is the same shape as Retrieve a procedure, and it accepts the same label-style query parameter.
Complimentary schemas
ReviewerGroup
{
"id": string;
"reviewers": Array<Reviewer>;
"actions": Array<ReviewerAction>;
};
Reviewer
{
"id": string;
"reviewer_ids": Array<ReviewerId>;
"is_required": (optional) true;
}
ReviewerId
{
type: string;
value: string;
};
ReviewerAction
{
"type": string;
"reviewer_id": string;
"user_id": string;
"approved_at": string;
}
Tag
{
"key": string;
"name": string;
}
DraftVariable
{
"id": string;
"name": string;
"type": 'input',
"rule": (optional) Rule;
"units": (optional) string;
"input_type": (optional) 'text' | 'number' | 'checkbox';
"inputType": (optional) 'text' | 'number' | 'checkbox' | 'timestamp' | 'attachment' | 'list' | 'select' | 'external_item' | 'external_search' | 'multiple_choice';
"redlines": (optional) Array<BlockRedline>;
"version": (optional) 2;
"dateTimeType": (optional) 'date' | 'time' | 'datetime';
"include_in_summary": (optional) boolean;
"list": (optional) string;
"options": (optional) Array<string>;
"external_item_type": (optional) string;
"dictionary_id": (optional) number;
"external_search_type": (optional) {
"data_type_dictionary_id": (optional) number;
"data_type": string;
"filter_options": Array<string>;
};
}
BlockRedline
DraftVariable | {
"id": string;
"type": 'alert' | 'text';
"subtype": (optional) 'note' | 'caution' | 'warning';
"text": (optional) string;
}
DraftHeader
{
"id": string;
"name": string;
"content": Array<DraftHeaderBlock>;
"header_field_redlines": (optional) {
[field: string]: Array<DraftFieldRedline>;
};
"redlines": (optional) Array<DraftHeaderBlock | DraftFieldRedline>;
};
DraftHeaderBlock
{
"id": string;
"type": "alert" | "attachment" | "text";
"subtype": (optional) "note" | "caution" | "warning";
"text": string;
"redlines": (optional) Array<BlockRedline>;
}
DraftFieldRedline
{
redline_id: (optional) string;
created_at: (optional) string;
user_id: (optional) string;
redlineIndex: number;
name: string;
redlineId: (optional) string;
createdAt: (optional) string;
userId: (optional) string;
}
RedlineAction
{
"redline_id": string;
"state": 'accepted' | 'rejected' | 'resolved';
"resolved_at": string;
"resolved_by": string;
"resolution_procedure_rev_num": (optional) number;
"release_procedure_rev_num": (optional) number;
}
DraftSection
{
"id": string;
"name": string;
"display_label": (optional) string;
"headers": (optional) Array<DraftHeader>;
"steps": Array<DraftStep | DraftAddedStep>;
"snippet_id": (optional) string;
"snippet_name": (optional) string;
"snippet_rev": (optional) number;
"snippet_ids_map": (optional) Object<string, string>;
"shows_snippet_detached_message": (optional) boolean;
}
DraftStep
{
"id": string;
"name": string;
"display_label": (optional) string;
"type": (optional) string;
"headers": (optional) Array<DraftHeader>;
"timing": (optional) string;
"duration": (optional) string;
"location": (optional) string;
"channel": (optional) string;
"content": Array<DraftStepBlock>;
"signoffs": Array<Signoff>;
"dependencies": (optional) Array<StepDependency>;
"conditionals": (optional) Array<Conditional>;
"actions": (optional) Array<StepAction>;
"step_field_redlines": (optional) {
[field: string]: Array<DraftFieldRedline>;
};
"shows_snippet_detached_message": (optional) boolean;
"snippet_id": (optional) string;
"snippet_name": (optional) string;
"snippet_rev": (optional) number;
"snippet_ids_map": (optional) Object<string, string>;
"redline_id": (optional) string;
"precedingStepId": (optional) string;
"requires_previous": (optional) boolean;
"created_during_run": true;
"created_at": string;
"created_by": string;
}
StepDependency
{
"id": string;
"dependent_ids": Array<string>;
}
Conditional
{
"id": string;
"source_type": 'step' | 'content';
"state": 'completed' | 'failed';
"content_id": null;
"source_id": string;
"target_id": string;
"target_type": 'step';
}
StepAction
{
"type": 'signoff';
"user_id": string;
"timestamp": string;
"conditional_value": (optional) string;
"signoff_id": string;
"operator": string;
}
ReleaseNote
{
"user_id": string;
"timestamp": string;
"text": string;
}
ReviewComment
{
"id": string;
"reference_id": string;
"redline_id": (optional) string;
"text": string;
"user_id": string;
"created_at": string;
"type": (optional) 'redline_comment';
"source_run_id": (optional) string;
"resolved": (optional) boolean;
"resolved_by": (optional) string;
"resolved_at": (optional) string;
"resolution_procedure_rev_num": (optional) number;
}
Approval
{
"user_id": string;
"approved_at": string;
}
Settings
{
"print_settings": (options) {
"header": (options) PrintContent;
"footer": (options) PrintContent;
};
}
PrintContent
{
"text": (optional) string;
"variable_id": (optional) string;
}
DraftStepBlockis a union type representing the content blocks that can appear inside a step'scontentarray. Each block has atypefield that distinguishes what kind of block variant it is. All content blocks extend a common baseBaseBlockContent:BaseBlockContent
{
"type": string;
"display_label": (optional) string;
"original_id": (optional) string;
"added": (optional) boolean;
"snippet_id": (optional) string;
"snippet_name": (optional) string;
"snippet_rev": (optional) string;
"snippet_ids_map": (optional) Object<string, string>;
}
DraftTelemetryBlock
{
"type": "telemetry";
"key": "parameter" | "custom";
"name": (optional) string;
"units": (optional) string;
"parameter_id": (optional) number;
"dictionary_id": (optional) number;
"dictionary_group_id": (optional) number;
"parameter_type": (optional) "float" | "int" | "string" | "bool" | "enum";
"rule": "=" | "≠" | "<" | ">" | "≤" | "≥" | "range" | "";
"value": string;
"expression": (optional) string;
"range": (optional) Range;
"include_in_summary": (optional) boolean;
}
Range
{
"min": string | number;
"max": string | number;
"include_min": (optional) boolean;
"include_max": (optional) boolean;
}
DraftCommandingBlock
{
"type": "commanding";
"key": "command" | "COSMOS";
"name": (optional) string;
"command_id": (optional) number;
"dictionary_id": (optional) number;
"dictionary_group_id": (optional) number;
"arguments": (optional) CommandingBlockArguments;
"include_in_summary": (optional) boolean;
}
CommandingBlockArguments
{
"[argument_name]": string | AttachmentArgValue | null;
}
AttachmentArgValue
{
"attachment_id": string;
"name": string;
"content_type": string;
}
DraftInventoryCheckoutBlock
{
"type": "inventory_checkout";
"items": Array<InventoryBlockPart>;
"include_in_summary": (optional) boolean;
}
DraftInventoryCheckinBlock
{
"type": "inventory_checkin";
"items": Array<InventoryBlockPart>;
"include_in_summary": (optional) boolean;
}
InventoryBlockPart
{
"part_id": string;
"revision_id": string;
"quantity": number;
"details": (optional) Array<CustomFieldValue>;
}
On check-out and check-in blocks, details carries the workspace's custom field column values for the row. Kit inventory blocks ignore details. See Recorded Custom Field Values for the wire shape.
DraftKitInventoryBlock
{
"type": "kit_inventory";
"assembly_part_id": string;
"assembly_part_revision_id": string;
"items": Array<InventoryBlockPart>;
"item_groups": (optional) Array<KitItemGroup>;
"include_in_summary": (optional) boolean;
"restrict_to_run": (optional) boolean;
}
KitItemGroup
{
"id": string;
"name": string;
"description": (optional) string;
}
DraftDeleteInventoryKitBlock
{
"type": "delete_inventory_kit";
"assembly_part_id": string;
"assembly_part_revision_id": string;
"include_in_summary": (optional) boolean;
}
DraftAddInventoryBlock
{
"type": "add_inventory";
"items": Array<AddInventoryItem>;
"include_in_summary": (optional) boolean;
"record_weight": (optional) boolean;
}
AddInventoryItem
{
"id": string;
"part_id": string;
"part_revision_id": (optional) string;
"amount": number;
}
DraftUpdateComponentsBlock
{
"type": "update_components";
"assembly_part_id": string;
"assembly_part_revision_id": string;
"include_in_summary": (optional) boolean;
"access_mode": (optional) "all" | "restrict";
"record_weight": (optional) boolean;
"defined_components": (optional) Array<UpdateComponentsDefinedComponent>;
}
access_mode defaults to "all", which places no limit on the components operators update. With "restrict", operators can only make the changes listed in defined_components.
UpdateComponentsDefinedComponent
{
"part_id": string;
"part_revision_id": string;
"ref_des_group_id": (optional, nullable) string;
"update_type": "add" | "remove" | "swap";
"quantity": number;
"details": (optional) Array<CustomFieldValue>;
}
details carries the workspace's custom field column values for the defined component. See Recorded Custom Field Values for the wire shape.
DraftInventoryDetailInputBlock
{
"type": "inventory_detail_input";
"part_revision_id": string;
"detail_id": string;
"part_id": (optional) string;
"include_in_summary": (optional) boolean;
}
DraftViewInventoryDetailsBlock
{
"type": "view_inventory_details";
"selected_field_ids": Array<string>;
"selected_system_fields": (optional) Array<"unit_cost_cents" | "weight_grams" | "location_id">;
"part_revision_id": (optional) string;
"part_id": (optional) string;
"reference": (optional) ViewInventoryDetailsBlockReference;
"include_in_summary": (optional) boolean;
}
ViewInventoryDetailsBlockReference
{
"id": string;
"item_id": (optional) string;
"label": string;
"scope": (optional) "item" | "block";
"part_id": (optional) string;
"part_revision_id": (optional) string;
}
DraftAlertBlock
{
"type": "alert";
"subtype": "note" | "caution" | "warning";
"text": string;
"tokens": (optional) Array<ExpressionToken>;
}
DraftAttachmentBlock
{
"type": "attachment";
"attachment_id": (optional) string;
"name": (optional) string;
"file": (optional) Blob;
"caption": (optional) string;
"size": (optional) "small" | "best_fit" | "original";
"content_type": (optional) string;
"display_style": (optional) "inline" | "to_the_side";
}
DraftTextBlock
{
"type": "text";
"text": string;
"tokens": (optional) Array<ExpressionToken>;
}
ExpressionToken
{
"type": "text" | "reference" | "function" | "eval";
"value": string;
"reference_subtype": (optional) "number" | "timestamp" | "text" | "list" | "select";
"timestamp_subtype": (optional) "date" | "time" | "datetime";
"reference_id": (optional) string;
"field_index": (optional) number;
"table_reference": (optional) {
"row_id": string;
"column_id": string;
};
"custom_field": (optional) {
"field_system_id": string;
"block_id": string;
"type": string;
"custom_field_type": string;
};
"function_name": (optional) string;
"function_params": (optional) Array<Array<ExpressionToken>>;
"tokens": (optional, type 'text' or 'reference') Array<ExpressionToken>;
"list": (optional) string;
}
DraftExpressionBlock
{
"type": "expression";
"name": string;
"tokens": Array<ExpressionToken>;
"rule": (optional) Rule;
"include_in_summary": (optional) boolean;
"hide_in_run": (optional) boolean;
}
Rule
{
"op": "=" | "≠" | "<" | ">" | "≤" | "≥" | "range" | "";
"value": string | number;
"range": (optional) Range;
}
DraftFieldInputBlock
DraftFieldInputTextBlock
| DraftFieldInputNumberBlock
| DraftFieldInputCheckboxBlock
| DraftFieldInputTimestampBlock
| DraftFieldInputAttachmentBlock
| DraftFieldInputSketchBlock
| DraftFieldInputScanCodeBlock
| DraftFieldInputSettingsListBlock
| DraftFieldInputCustomListBlock
| DraftFieldInputExternalDataBlock
| DraftFieldInputExternalSearchBlock
| DraftFieldInputMultipleChoiceBlock
DraftFieldInputTextBlock
{
"type": "input";
"inputType": "text";
"name": string;
"units": (optional) string;
"include_in_summary": (optional) boolean;
"default_value": (optional) string;
}
DraftFieldInputNumberBlock
{
"type": "input";
"inputType": "number";
"name": string;
"units": string;
"rule": (optional) Rule;
"include_in_summary": (optional) boolean;
"default_value": (optional) number;
}
DraftFieldInputCheckboxBlock
{
"type": "input";
"inputType": "checkbox";
"name": string;
"include_in_summary": (optional) boolean;
"default_value": (optional) boolean;
}
DraftFieldInputTimestampBlock
{
"type": "input";
"inputType": "timestamp";
"name": string;
"dateTimeType": "date" | "time" | "datetime";
"include_in_summary": (optional) boolean;
}
DraftFieldInputAttachmentBlock
{
"type": "input";
"inputType": "attachment";
"name": string;
"include_in_summary": (optional) boolean;
}
DraftFieldInputSketchBlock
{
"type": "input";
"inputType": "sketch";
"name": string;
"include_in_summary": (optional) boolean;
}
DraftFieldInputScanCodeBlock
{
"type": "input";
"inputType": "scan_code";
"name": string;
"include_in_summary": (optional) boolean;
}
DraftFieldInputSettingsListBlock
{
"type": "input";
"inputType": "list";
"name": string;
"list": string;
"include_in_summary": (optional) boolean;
"default_value": (optional) string;
}
DraftFieldInputCustomListBlock
{
"type": "input";
"inputType": "select";
"name": string;
"options": Array<string>;
"include_in_summary": (optional) boolean;
"default_value": (optional) string;
}
DraftFieldInputExternalDataBlock
{
"type": "input";
"inputType": "external_item";
"name": string;
"external_item_type": string;
"dictionary_id": (optional) number;
"include_in_summary": (optional) boolean;
}
DraftFieldInputExternalSearchBlock
{
"type": "input";
"inputType": "external_search";
"name": string;
"external_search_type": {
"data_type_dictionary_id": (optional) number;
"data_type": string;
"filter_options": Array<string>;
};
"include_in_summary": (optional) boolean;
}
DraftFieldInputMultipleChoiceBlock
{
"type": "input";
"inputType": "multiple_choice";
"name": string;
"options": Array<string>;
"include_in_summary": (optional) boolean;
"default_value": (optional) string;
}
DraftFieldInputTableBlock
{
"type": "field_input_table";
"fieldsPerRow": number;
"include_in_summary": (optional) boolean;
"fields": Array<DraftFieldInputTextBlock | DraftFieldInputScanCodeBlock | DraftFieldInputNumberBlock | DraftFieldInputCheckboxBlock | DraftFieldInputTimestampBlock | DraftFieldInputCustomListBlock | DraftFieldInputSettingsListBlock>;
}
DraftTableInputBlock
{
"type": "table_input";
"sub_type": (optional) "" | "test_point";
"columns": Array<TableColumn>;
"row_metadata": (optional) Array<RowMetadata>;
"rows": number;
"cells": (optional) Array<Array<TableCell>>;
"include_in_summary": (optional) boolean;
}
TableColumn
{
"id": (optional) string;
"name": string;
"column_type": (optional) "text" | "input" | "signoff" | "comment" | "test_point";
"input_type": (optional) "text" | "number" | "checkbox" | "list";
"units": string;
"width": (optional) number;
"list": (optional) string;
"allow_input_after_signoff": (optional) boolean;
"disabled": (optional) boolean;
}
RowMetadata
{
"id": string;
}
DraftExternalItemBlock
{
"type": "external_item";
"dictionary_id": (optional) number;
"item_type": string;
"item_id": string;
"item_url": (optional) string;
"item_valid": (optional) boolean;
"item": (optional) {
"id": string;
"name": (optional) string;
"label": (optional) string;
"url": (optional) string;
"valid": (optional) boolean;
};
}
DraftInputReferenceBlock
{
"type": "reference";
"subtype": "input";
"reference": string;
"sub_reference": (optional) string;
"include_in_summary": (optional) boolean;
}
DraftJumpToBlock
{
"type": "jump_to";
"jumpToId": string;
}
DraftProcedureLinkBlock
{
"type": "procedure_link";
"procedure": string;
"section": string;
"run": (optional) string;
}
DraftRequirementBlock
{
"type": "requirement";
"requirement_id": string;
"label": string;
"subType": (optional) "manual" | "jama" | "epsilon3";
"requirement_details": (optional) RequirementIntegrationDetails;
"metadata": object;
}
RequirementIntegrationDetails
{
"id": string;
"name": string;
"description": string;
"link": string;
"version": (optional) string;
}
DraftTestCasesBlock
{
"type": "test_cases";
"items": Array<object>;
}
DraftPartBuildBlock
{
"type": "part_build";
"part_id": string;
"part": (optional) InternalPart;
"items": Array<PartBuildItem>;
"include_in_summary": (optional) boolean;
}
InternalPart
{
"id": string;
"rev": string;
"part_no": string;
"name": string;
"description": string;
"release_state": "draft" | "in_review" | "released";
"procedures": Array<object>;
"tracking": "serial" | "lot" | "none";
"allow_decimal_quantities": boolean;
"units": string | null;
"low_inventory_threshold": (optional) number;
"unit_price_cents": (optional) number;
"components": Array<object>;
"component_groups": Array<object>;
"usage_types": (optional) Array<UsageType>;
"details": (optional) Array<object>;
"revisions": Array<object>;
"assembly": boolean;
"procurement_type": "make" | "buy" | "make or buy";
"image": (optional) object;
"attachments": (optional) Array<object>;
"project_id": (optional) string | null;
"reviewer_groups": (optional) Array<ReviewerGroup>;
"is_approved": (optional) boolean;
"top_level_parent_boms": (optional) Array<object | null>;
"active_build_runs": (optional) Array<object>;
"procedure_occurrences": (optional) Array<object>;
"formula_sequence": (optional) string;
"preferred_vendor": (optional) object;
"demand_statistics": (optional) object;
}
PartBuildItem
{
"id": string;
"part_id": string;
"part_no": (optional) string;
"name": (optional) string;
"part_index": (optional) number;
"prefix": (optional) string;
"serial": (optional) string;
"amount": number;
"revision": (optional) string;
"revision_id": (optional) string;
"tracking": (optional) string;
"image": (optional) {
"name": string;
"content_type": string;
"attachment_id": string;
};
"assembly": (optional) boolean;
"project_id": (optional) string;
"units": (optional) string;
}
DraftPartUsageBlock
{
"type": "part_usage";
"part_id": string;
"usage_types": Array<UsageType>;
"apply_to_components": (optional) boolean;
"include_in_summary": (optional) boolean;
}
UsageType
{
"id": (optional) string;
"name": string;
"limit": (optional) number;
}
DraftPartKitBlock
{
"type": "part_kit";
"items": Array<KitPart>;
"item_groups": (optional) Array<KitItemGroup>;
"include_in_summary": (optional) boolean;
}
KitPart
{
"id": string;
"part_id": string;
"amount": number;
"part_no": (optional) string;
"name": (optional) string;
"revision": (optional) string;
"revision_id": (optional) string;
"tracking": (optional) string;
}
DraftBomViewBlock
{
"type": "bom_view";
"assembly_part_id": string;
"assembly_part_revision_id": string;
}
DraftToolCheckOutBlock
{
"type": "tool_check_out";
"tool_id": number;
"include_in_summary": (optional) boolean;
}
DraftToolCheckInBlock
{
"type": "tool_check_in";
"tool_id": number;
"include_in_summary": (optional) boolean;
}
DraftToolUsageBlock
{
"type": "tool_usage";
"tool_id": number;
"usage_type_id": number;
"include_in_summary": (optional) boolean;
}
Update draft steps
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v1/procedures/index_sampleProcedureId:pending/steps',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
'steps': [
{
'id': 'step_abc123',
'name': 'Verify bus voltage',
'content': [
{'id': 'cont_def456', 'type': 'text', 'text': 'Updated instructions'}
],
'signoffs': [
{'id': 'so_ghi789', 'operators': []}
]
}
]
}
)
curl -X PATCH \
'https://api.epsilon3.io/v1/procedures/index_sampleProcedureId:pending/steps' \
-u "$EPSILON3_API_KEY": \
-H 'Content-Type: application/json' \
-d '{"steps": [{"id": "step_abc123", "name": "Verify bus voltage", "content": [{"id": "cont_def456", "type": "text", "text": "Updated instructions"}], "signoffs": [{"id": "so_ghi789", "operators": []}]}]}'
$apiUrl = "https://api.epsilon3.io/v1/procedures/index_sampleProcedureId:pending/steps"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
$body = @{
steps = @(
@{
id = "step_abc123"
name = "Verify bus voltage"
content = @(@{ id = "cont_def456"; type = "text"; text = "Updated instructions" })
signoffs = @(@{ id = "so_ghi789"; operators = @() })
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body -ContentType "application/json"
PATCH https://api.epsilon3.io/v1/procedures/:draft-id/steps
Replaces the bodies of up to 100 existing steps in a draft in one atomic write. The :draft-id is the draft document's _id as returned by Retrieve a draft procedure (the form index_<procedure-id>:pending).
Each entry in steps is a complete step body that replaces the stored step with the same id, wherever it currently lives — a step that was concurrently moved to another section is still updated in place. Steps whose id no longer exists in the draft are reported as not_found without failing the rest of the batch.
This endpoint only updates existing steps. To add or remove steps, or to update more than 100 steps in one write, use Update draft structure.
Request body
| Parameter | Type | Description |
|---|---|---|
steps |
object[] | Full step bodies, each located by its own id. A step requires id, name (may be empty), a content array of blocks (each with a type), and a signoffs array (each with an id and an operators array). |
Example Response Body
{
"rev": "43",
"results": [
{ "step_id": "step_abc123", "status": "updated" },
{ "step_id": "step_gone", "status": "not_found" }
]
}
Response body
| Field | Type | Description |
|---|---|---|
rev |
string | The draft's new revision after the write. |
results |
object[] | Per-step outcome: updated or not_found. |
Errors
| Status | Meaning |
|---|---|
400 |
The id in the URL is not a draft id. |
404 |
No draft exists with this id. |
409 |
The draft is in review and cannot be edited. |
422 |
A step body is invalid, the batch contains two bodies for the same step, or a content block or signoff id duplicates one used elsewhere in the procedure. |
Update draft structure
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v1/procedures/index_sampleProcedureId:pending/structure',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
'procedure': {
'_id': 'index_sampleProcedureId:pending',
'name': 'Renamed procedure',
'code': 'PROC-001',
'description': 'Updated description',
'sections': [
{
'id': 'sctn_abc123',
'name': 'Section A',
'steps': [
{'id': 'step_abc123'},
{
'name': 'A brand new step',
'content': [{'type': 'text', 'text': 'New step instructions'}],
'signoffs': []
}
]
}
]
}
}
)
curl -X PATCH \
'https://api.epsilon3.io/v1/procedures/index_sampleProcedureId:pending/structure' \
-u "$EPSILON3_API_KEY": \
-H 'Content-Type: application/json' \
-d '{"procedure": {"_id": "index_sampleProcedureId:pending", "name": "Renamed procedure", "code": "PROC-001", "description": "Updated description", "sections": [{"id": "sctn_abc123", "name": "Section A", "steps": [{"id": "step_abc123"}, {"name": "A brand new step", "content": [{"type": "text", "text": "New step instructions"}], "signoffs": []}]}]}}'
$apiUrl = "https://api.epsilon3.io/v1/procedures/index_sampleProcedureId:pending/structure"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
$body = @{
procedure = @{
_id = "index_sampleProcedureId:pending"
name = "Renamed procedure"
code = "PROC-001"
description = "Updated description"
sections = @(
@{
id = "sctn_abc123"
name = "Section A"
steps = @(
@{ id = "step_abc123" },
@{
name = "A brand new step"
content = @(@{ type = "text"; text = "New step instructions" })
signoffs = @()
}
)
}
)
}
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body -ContentType "application/json"
PATCH https://api.epsilon3.io/v1/procedures/:draft-id/structure
Updates procedure-level fields and, optionally, the section/step skeleton of a draft. The merge is sparse: writable fields omitted from the payload keep their stored values, and fields the API does not manage (state, approvals, revision counters, and other server-owned fields) are ignored.
procedure._id is required and must match the draft id in the URL.
Procedure-level fields. The writable fields are name, code, description, version, owner, project_id, variables, headers, conditionals, details, settings, default_view_mode, dictionary_id, data_sources, auto_procedure_id_enabled, repeat_step_enabled, skip_step_enabled, step_suggest_edits_enabled, is_strict_signoff_enabled, automation_enabled, start_run_signoffs_groups, end_run_signoffs_groups, release_flow_id, review_group_id, review_type, enhanced_review_type, enhanced_review_enabled, result, part_list, bom_view, test_case_list, risks, comments, and redline_actions. Requests authenticated with a user session additionally require admin permission to change admin-gated settings; API key requests act as admins.
Removing fields. The block fields result, part_list, bom_view, and test_case_list, and the release process fields release_flow_id, review_group_id, and review_type, are removed from the draft by sending them as null. Omitting them keeps the stored values.
Comments. comments are merged by id — entries you send are added or updated, and stored comments you omit are kept.
Sections. When sections is omitted, the stored skeleton and all step bodies are untouched. When present, it is authoritative for section membership and order: sections and steps omitted from it are removed from the draft. Steps may be sent as {"id": ...} stubs, which keep the stored step body exactly as it is, or as full bodies, which replace it. A new section or step may omit its id (one is generated and returned in the response) or provide one with the sctn_/step_ prefix. A stub referencing a step the draft does not have is rejected — new steps require a full body.
Example Response Body
{
"rev": "44",
"procedure": {
"_id": "index_sampleProcedureId:pending",
"_rev": "44",
"name": "Renamed procedure",
"...": "the full saved draft document"
}
}
Response body
| Field | Type | Description |
|---|---|---|
rev |
string | The draft's new revision after the write. |
procedure |
object | The full saved draft, including generated ids for new sections and steps. Same shape as Retrieve a draft procedure. |
Errors
| Status | Meaning |
|---|---|
400 |
The id in the URL is not a draft id, or procedure._id does not match it. |
404 |
No draft exists with this id. |
409 |
The draft is in review and cannot be edited. |
422 |
The payload is invalid: duplicate section or step ids, a stub for a step the draft does not have, a section without steps, or a content block or signoff id duplicated across steps. |
Reorder draft steps
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v1/procedures/index_sampleProcedureId:pending/step-order',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
'sections': [
{'section_id': 'sctn_abc123', 'step_ids': ['step_2', 'step_1']},
{'section_id': 'sctn_def456', 'step_ids': ['step_3']}
]
}
)
curl -X PATCH \
'https://api.epsilon3.io/v1/procedures/index_sampleProcedureId:pending/step-order' \
-u "$EPSILON3_API_KEY": \
-H 'Content-Type: application/json' \
-d '{"sections": [{"section_id": "sctn_abc123", "step_ids": ["step_2", "step_1"]}, {"section_id": "sctn_def456", "step_ids": ["step_3"]}]}'
$apiUrl = "https://api.epsilon3.io/v1/procedures/index_sampleProcedureId:pending/step-order"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
$body = @{
sections = @(
@{ section_id = "sctn_abc123"; step_ids = @("step_2", "step_1") },
@{ section_id = "sctn_def456"; step_ids = @("step_3") }
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body -ContentType "application/json"
PATCH https://api.epsilon3.io/v1/procedures/:draft-id/step-order
Reorders sections and steps, including moves between sections, without touching any step content. The payload is the complete layout as ids only, and it must be an exact permutation of the draft's current sections and steps — every stored section and step appears exactly once, and nothing else. This makes it impossible for a reorder computed against a stale copy to drop a step another editor just added: the write is rejected instead.
Request body
| Parameter | Type | Description |
|---|---|---|
sections |
object[] | The full layout, in order. Each entry has section_id and step_ids (the section's steps, in order). |
Example Response Body
{
"rev": "45"
}
Errors
| Status | Meaning |
|---|---|
400 |
The id in the URL is not a draft id. |
404 |
No draft exists with this id. |
409 |
The draft is in review, or the layout is stale — it does not exactly match the draft's current sections and steps. Refetch the draft and retry. |
Delete a draft step
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/procedures/index_sampleProcedureId:pending/steps/step_abc123',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X DELETE \
'https://api.epsilon3.io/v1/procedures/index_sampleProcedureId:pending/steps/step_abc123' \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/procedures/index_sampleProcedureId:pending/steps/step_abc123"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers $headers
DELETE https://api.epsilon3.io/v1/procedures/:draft-id/steps/:step-id
Removes one step from the draft and clears references to it from other steps (dependencies, conditionals, and jump-to targets). Deleting a step that has already been removed succeeds without changing the draft, so retries are safe.
Example Response Body
{
"rev": "46"
}
Errors
| Status | Meaning |
|---|---|
400 |
The id in the URL is not a draft id. |
404 |
No draft exists with this id. |
409 |
The draft is in review and cannot be edited. |
Project Schedule API
The Project Schedule API manages schedule events for projects and work orders: their planned dates, recurrence, resource assignments, and inter-event dependencies. Each event belongs to an owning entity (a project or a work order) and either represents an underlying domain entity (work order, run, purchase order, shipment, issue, procedure) or is a free-standing ad-hoc event you create directly.
Events
The endpoints in this group all return the Event object. It is described once here and referenced from each endpoint.
The Event object
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"owner_entity_type": "project",
"owner_entity_id": "9NKegRHwzK",
"event_entity_type": "adhoc",
"event_entity_id": null,
"linked_entity_type": "run",
"linked_entity_id": "aZ8kQ2mLpV",
"planned_start_date": "2026-06-20T14:00:00.000Z",
"planned_end_date": "2026-06-20T15:00:00.000Z",
"actual_start_date": "2026-06-20T14:05:00.000Z",
"actual_end_date": null,
"entity_name": "Design review",
"linked_run_data": {
"starttime": "2026-06-20T14:05:00.000Z",
"completedAt": null,
"code": "PFC-001",
"name": "Pre-flight Checkout",
"run_number": 7,
"state": "running",
"status": "running",
"total_steps": 24,
"completed_steps": 9,
"not_required_steps": 1,
"skipped_steps": 0,
"failed_steps": 0,
"project_id": "9NKegRHwzK"
},
"title": "Design review",
"description": "Critical design review meeting",
"created_at": "2026-06-15T09:30:00.000Z",
"updated_at": "2026-06-15T09:30:00.000Z",
"created_by": "usr_7TqLmZ0pQ2",
"assignments": [],
"dependencies": [],
"recurrence_id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"is_recurrence_master": false,
"rrule": "FREQ=WEEKLY;COUNT=4",
"details": [
{
"id": "fld_3Hq8Lm2Pz",
"field_id": "priority",
"name": "Priority",
"type": "Custom List",
"metadata": { "options": ["High", "Medium", "Low"] },
"value": { "recorded": "High" }
}
]
}
Event fields
id: System generated UUID of the event.owner_entity_type:work_orderorproject— the type of entity that owns the event.owner_entity_id: Id of the owning work order or project.event_entity_type: The underlying domain entity the event represents — one ofprocedure,run,purchase_order,shipment,work_order,issue,project, oradhoc.adhocis a free-standing schedule event.event_entity_id: Id of the underlying entity. Absent foradhocevents.linked_entity_type/linked_entity_id: Optional link from an ad-hoc event to aprocedureorrun. Schedule-generated events may also reportpart_revision, which cannot be set through the API.planned_start_date/planned_end_date: ISO 8601 planned window. For work-order-owned events these reflect the work order's dates.actual_start_date/actual_end_date: ISO 8601 actuals derived from the linked entity (e.g. a run's start/finish). Absent when not applicable.entity_name: Display name derived from the underlying entity (e.g.WO-1042 Avionics Assembly). Falls back to the event'stitle.linked_run_data: Present only when a run is the event's entity or linked to it, and the run is in a project the caller can access. Summarizes the run's progress (state/status and step counts) and carries the run'sproject_id.title/description: Free-text title and description.created_at/updated_at: ISO 8601 timestamps.created_by: Id of the user the event is attributed to.assignments: Array of Event Assignment objects (may be empty).dependencies: Array of dependency objects (may be empty). Each containsid,predecessor_event_id,successor_event_id,dependency_type,created_at,updated_at, andcreated_by.Retrieve Eventadditionally includespredecessor_titleandsuccessor_title.recurrence_id: Present on events that belong to a recurrence series.is_recurrence_master:truefor the series master event.rrule: The series recurrence rule.details: Array of Recorded Custom Field Values for the fields on the workspace's Schedule Event form. Every event type can carry custom field values. Fields with no recorded value are omitted.schedule_plan_id: Present only on draft events that belong to a schedule plan. Draft events cannot hold custom field values.
Project access
Events are scoped to the caller's project access. Listing events returns only events owned by a project the caller can access, or by a work order whose project the caller can access; events outside that scope are omitted rather than rejected, so a list filtered to an inaccessible owner returns an empty array. Retrieving a single event the caller cannot access returns 403.
Within an event the caller can see, references to other entities are filtered the same way. When an event references a run, procedure, work order, or issue in a project the caller cannot access, reads still return the reference ids but omit the entity's data — linked_run_data and run-derived actual_start_date/actual_end_date are absent, and entity_name (and dependency titles) fall back to the event's stored title. Writes are checked the same way: creating an event for, or linking to, a run or procedure the caller cannot access fails with 404 as if it did not exist. Entities without a project require team-level access.
Recurring events
Recurrence is materialized, not computed on the fly. Supplying an rrule when creating an event produces a master event plus one concrete event row per occurrence, all sharing a recurrence_id. Consequences worth noting:
- Only
adhocevents can recur. Because every occurrence copies the master's entity identity, a recurring event that represents an entity would collide with itself, so anrruleon any otherevent_entity_typereturns400. GET /eventsreturns the materialized occurrences and hides the master.GET /events/:eventIdcan fetch the master by id.- An
rrulemust terminate — it must contain aCOUNT(at most1000) or anUNTILwithin one year — and requires aplanned_start_date. - Update the whole series with
apply_to_series; detach a single occurrence withdetach_from_series; delete the whole series with?deleteSeries=true. - Dependencies cannot be created on recurring events.
- New occurrences copy the master's custom field
details, both when the series is created and when it is regenerated. Because regeneration copies from the master, a value set on a single occurrence is replaced when the series'rrulechanges.
List Events
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/project-schedule/events',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
params={
"ownerEntityType": "project",
"ownerEntityId": "9NKegRHwzK"
}
)
curl -X GET \
"https://api.epsilon3.io/v1/project-schedule/events?ownerEntityType=project&ownerEntityId=9NKegRHwzK" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/project-schedule/events?ownerEntityType=project&ownerEntityId=9NKegRHwzK"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/project-schedule/events
List schedule events for your workspace, optionally filtered by owner, underlying entity, assigned resource, or explicit ids. Results are ordered by planned_start_date ascending.
Parameters
All parameters are optional query parameters.
ownerEntityType: Filter by owner type —work_orderorproject. Required ifownerEntityIdis provided.ownerEntityId: Id of the owning work order or project to filter by.eventEntityType: Filter by underlying entity type (procedure,run,purchase_order,shipment,work_order,issue,project, oradhoc). Required ifeventEntityIdis provided.eventEntityId: Id of the underlying entity to filter by.resourceType: Filter to events assigned a resource of this type —user,tool,location, orgroup. Required ifresourceIdis provided. When combined withresourceId, the resource must exist or the request returns404.resourceId: Id of the assigned resource to filter by.eventIds: Repeatable parameter listing explicit event ids to fetch (eventIds=a&eventIds=b). Each id must be a valid UUID.detail_{field_id}[:{op}]: Filter by a custom field value. See Custom field filters on events.
Providing an id parameter without its paired type parameter returns 422.
Example Response Body
{
"events": [
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"owner_entity_type": "project",
"owner_entity_id": "9NKegRHwzK",
"event_entity_type": "adhoc",
"planned_start_date": "2026-06-20T14:00:00.000Z",
"planned_end_date": "2026-06-20T15:00:00.000Z",
"entity_name": "Design review",
"title": "Design review",
"description": "Critical design review meeting",
"created_at": "2026-06-15T09:30:00.000Z",
"updated_at": "2026-06-15T09:30:00.000Z",
"created_by": "usr_7TqLmZ0pQ2",
"assignments": [
{
"id": "f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f",
"event_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"resource": {
"type": "user",
"resource": { "id": "usr_7TqLmZ0pQ2" },
"name": "Jane Engineer"
},
"allocation_percent": 50,
"notes": "Lead reviewer",
"created_at": "2026-06-15T09:30:00.000Z",
"assigned_by": "usr_7TqLmZ0pQ2"
}
],
"dependencies": [],
"recurrence_id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"rrule": "FREQ=WEEKLY;COUNT=4"
}
]
}
Response
- Status:
200 OK - Body:
{ "events": [ ... ] }, an array of Event objects ordered byplanned_start_dateascending.
Recurrence master events and draft (schedule-plan) events are excluded from the list, as are events whose owning project or work order the caller cannot access — see Project access. Each event's dependencies here lists only dependencies where the event is the predecessor; use Retrieve Event for the full bidirectional view.
Custom field filters on events
List Events accepts dynamic query parameters that filter results by the value of a field on the workspace's Schedule Event form. The parameter name encodes both the field and an optional operator.
detail_{field_id}=valuefilters by the event's custom field. Thefield_idis the stable identifier returned by list custom fields.- An operator suffix may be appended after a colon:
detail_{field_id}:{op}=value. When the suffix is omitted, the operator defaults toeq. - Repeating the same key creates an OR-set of accepted operands for that operator (e.g.,
detail_priority=High&detail_priority=Mediummatches events whosepriorityisHighorMedium). - Multiple different keys must all match.
- The empty string
""matches an event whose value was recorded and then cleared (value.recordedisnull). It does not match an event on which the field was never set.
Supported operators per custom field type:
| Field type | Allowed operators |
|---|---|
Number, Date, Time, Date Time |
eq, ne, gt, gte, lt, lte |
Text |
eq, ne, contains |
List, Custom List, Checkbox |
eq, ne |
External Data |
None — External Data fields cannot be filtered |
Operand format per type: ISO-8601 strings for Date / Time / Date Time, numeric strings for Number, plain strings for Text / List / Custom List, true or false for Checkbox.
contains matches Text values that include the case insensitive operand anywhere, does not accept empty strings or wildcards.
For List fields, an operand matches either the selected option's display text or its option_id (see Recorded Custom Field Values). Filtering by option_id keeps matching after an option is renamed. Values recorded without an option_id can only be matched by their text.
Retrieve Event
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
GET https://api.epsilon3.io/v1/project-schedule/events/:eventId
Retrieve a single event by id.
Parameters
eventId(path parameter): UUID of the event to retrieve.
Example Response Body
{
"event": {
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"owner_entity_type": "work_order",
"owner_entity_id": "7QmTpRxY3z",
"event_entity_type": "work_order",
"event_entity_id": "7QmTpRxY3z",
"planned_start_date": "2026-07-01T08:00:00.000Z",
"planned_end_date": "2026-07-10T17:00:00.000Z",
"actual_start_date": "2026-07-01T08:15:00.000Z",
"entity_name": "WO-1042 Avionics Assembly",
"created_at": "2026-06-15T09:30:00.000Z",
"updated_at": "2026-06-15T09:30:00.000Z",
"created_by": "usr_7TqLmZ0pQ2",
"assignments": [],
"dependencies": [
{
"id": "d1e2f3a4-b5c6-4d7e-8f90-1a2b3c4d5e6f",
"predecessor_event_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"successor_event_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"dependency_type": "finish_to_start",
"created_at": "2026-06-15T09:31:00.000Z",
"updated_at": "2026-06-15T09:31:00.000Z",
"created_by": "usr_7TqLmZ0pQ2",
"predecessor_title": "WO-1042 Avionics Assembly",
"successor_title": "Integration Test"
}
]
}
}
Response
- Status:
200 OK - Body:
{ "event": { ... } }, a single Event object.
Unlike List Events, the dependencies array here is bidirectional — it includes dependencies where the event is either the predecessor or the successor, and each entry carries predecessor_title and successor_title. A non-existent id returns 404.
Create Events
Sample Request (Single Event)
import requests
import os
data = {
"events": [
{
"owner_entity_type": "project",
"owner_entity_id": "9NKegRHwzK",
"event_entity_type": "adhoc",
"title": "Design review",
"planned_start_date": "2026-06-20T14:00:00.000Z",
"planned_end_date": "2026-06-20T15:00:00.000Z",
"created_by": "usr_7TqLmZ0pQ2"
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/project-schedule/events',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/project-schedule/events \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"events": [
{
"owner_entity_type": "project",
"owner_entity_id": "9NKegRHwzK",
"event_entity_type": "adhoc",
"title": "Design review",
"planned_start_date": "2026-06-20T14:00:00.000Z",
"planned_end_date": "2026-06-20T15:00:00.000Z",
"created_by": "usr_7TqLmZ0pQ2"
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/project-schedule/events"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
events = @(
@{
owner_entity_type = "project"
owner_entity_id = "9NKegRHwzK"
event_entity_type = "adhoc"
title = "Design review"
planned_start_date = "2026-06-20T14:00:00.000Z"
planned_end_date = "2026-06-20T15:00:00.000Z"
created_by = "usr_7TqLmZ0pQ2"
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body (Multiple Events with a Dependency)
{
"events": [
{
"owner_entity_type": "project",
"owner_entity_id": "9NKegRHwzK",
"event_entity_type": "adhoc",
"title": "Design review",
"planned_start_date": "2026-06-20T14:00:00.000Z",
"planned_end_date": "2026-06-20T15:00:00.000Z",
"created_by": "usr_7TqLmZ0pQ2",
"temporary_event_id": "tmp-1",
"assignments": [
{
"resource_type": "user",
"resource_id": "usr_7TqLmZ0pQ2",
"allocation_percent": 50,
"assigned_by": "usr_7TqLmZ0pQ2"
}
]
},
{
"owner_entity_type": "project",
"owner_entity_id": "9NKegRHwzK",
"event_entity_type": "adhoc",
"title": "Integration test",
"planned_start_date": "2026-06-21T09:00:00.000Z",
"planned_end_date": "2026-06-21T12:00:00.000Z",
"created_by": "usr_7TqLmZ0pQ2",
"temporary_event_id": "tmp-2"
}
],
"dependencies": [
{
"predecessor_temporary_event_id": "tmp-1",
"successor_temporary_event_id": "tmp-2",
"dependency_type": "finish_to_start",
"created_by": "usr_7TqLmZ0pQ2"
}
]
}
Example Response Body
{
"events": [
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"owner_entity_type": "project",
"owner_entity_id": "9NKegRHwzK",
"event_entity_type": "adhoc",
"planned_start_date": "2026-06-20T14:00:00.000Z",
"planned_end_date": "2026-06-20T15:00:00.000Z",
"entity_name": "Design review",
"title": "Design review",
"created_at": "2026-06-15T09:30:00.000Z",
"updated_at": "2026-06-15T09:30:00.000Z",
"created_by": "usr_7TqLmZ0pQ2",
"assignments": [
{
"id": "f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f",
"event_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"resource": { "type": "user", "resource": { "id": "usr_7TqLmZ0pQ2" }, "name": "Jane Engineer" },
"allocation_percent": 50,
"created_at": "2026-06-15T09:30:00.000Z",
"assigned_by": "usr_7TqLmZ0pQ2"
}
],
"dependencies": [],
"temporary_event_id": "tmp-1"
},
{
"id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"owner_entity_type": "project",
"owner_entity_id": "9NKegRHwzK",
"event_entity_type": "adhoc",
"planned_start_date": "2026-06-21T09:00:00.000Z",
"planned_end_date": "2026-06-21T12:00:00.000Z",
"entity_name": "Integration test",
"title": "Integration test",
"created_at": "2026-06-15T09:30:00.000Z",
"updated_at": "2026-06-15T09:30:00.000Z",
"created_by": "usr_7TqLmZ0pQ2",
"assignments": [],
"dependencies": [],
"temporary_event_id": "tmp-2"
}
],
"dependencies": [
{
"id": "d1e2f3a4-b5c6-4d7e-8f90-1a2b3c4d5e6f",
"predecessor_event_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"successor_event_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"dependency_type": "finish_to_start",
"created_at": "2026-06-15T09:30:01.000Z",
"updated_at": "2026-06-15T09:30:01.000Z",
"created_by": "usr_7TqLmZ0pQ2",
"predecessor_title": "Design review",
"successor_title": "Integration test"
}
]
}
POST https://api.epsilon3.io/v1/project-schedule/events
Create one or more events, and optionally dependencies between them, in a single atomic request. If any event or dependency is invalid, the entire batch is rejected and nothing is created.
Request Body
events: (required) Array of 1 to 500 event objects. Each contains:owner_entity_type: (required)work_orderorproject.owner_entity_id: (required) Id of the owning work order or project.event_entity_type: (optional) Underlying entity type. Defaults toadhoc.event_entity_id: (required for non-adhocevents) Id of the underlying entity, which must exist.linked_entity_type/linked_entity_id: (optional, ad-hoc events only) Link to aprocedureorrun. Both must be provided together.planned_start_date: (required wheneverplanned_end_dateorrruleis set) ISO 8601 start. Ad-hoc events may otherwise be left unscheduled (no dates) or start-only (a milestone). For work-order-owned events this is applied to the work order.planned_end_date: (optional) ISO 8601 end. When set,planned_start_dateis required.title: (required, non-empty, for ad-hoc events) Display title, up to 255 characters.description: (optional) Up to 4096 characters.created_by: (required) Id of the user the event is attributed to. Required because API-key requests are not made as a specific signed-in user.assignments: (optional) Resource assignments to create with the event (replicated to all recurrence occurrences). Each containsresource_type,resource_id,allocation_percent(0–100),assigned_by, and optionalnotes. See Assignments.rrule: (optional, ad-hoc events only) Recurrence rule. Must containCOUNT(≤ 1000) orUNTIL(within one year) and requiresplanned_start_date. Setting it on an event with any otherevent_entity_typereturns400. The total occurrences generated across the whole batch may not exceed 1000.temporary_event_id: (optional) A client-assigned placeholder, unique within the request, that dependencies in the same request can reference before the real id exists.details: (optional) Up to 100 custom field values to record on the event. See Custom field values on events. Recurring events copy them to every occurrence.
dependencies: (optional) Array of up to 500 dependency objects to create alongside the events. Each contains:- A predecessor reference — exactly one of
predecessor_temporary_event_id,predecessor_event_id, orpredecessor_entity. - A successor reference — exactly one of
successor_temporary_event_id,successor_event_id, orsuccessor_entity. predecessor_entity/successor_entity: Entity reference of the form{ entity_type, entity_id, owner_entity_type, owner_entity_id }, used to find-or-create the referenced event.dependency_type: (optional) One offinish_to_start,start_to_start,finish_to_finish,start_to_finish. Defaults tofinish_to_start.created_by: (required) Id of the user the dependency is attributed to.
- A predecessor reference — exactly one of
Response
- Status:
201 Created - Body:
{ "events": [ ... ], "dependencies": [ ... ] }.eventsare the created Event objects in request order, each echoing itstemporary_event_id; for recurring events only the master is returned (occurrences are not).dependencieslists the created dependency rows.
Update Event
Sample Request
import requests
import os
data = {
"planned_start_date": "2026-06-22T14:00:00.000Z",
"planned_end_date": "2026-06-22T15:30:00.000Z",
"title": "Design review (rescheduled)",
"apply_to_series": True
}
requests.patch(
url='https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"planned_start_date": "2026-06-22T14:00:00.000Z",
"planned_end_date": "2026-06-22T15:30:00.000Z",
"title": "Design review (rescheduled)",
"apply_to_series": true
}'
$apiUrl = "https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
planned_start_date = "2026-06-22T14:00:00.000Z"
planned_end_date = "2026-06-22T15:30:00.000Z"
title = "Design review (rescheduled)"
apply_to_series = $true
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"planned_start_date": "2026-06-22T14:00:00.000Z",
"planned_end_date": "2026-06-22T15:30:00.000Z",
"title": "Design review (rescheduled)",
"apply_to_series": true
}
PATCH https://api.epsilon3.io/v1/project-schedule/events/:eventId
Update one or more fields of an event. Omitted fields are left unchanged.
Parameters
eventId(path parameter): Id of the event to update. Must reference an existing event.
Request Body
All fields are optional; supply at least one.
planned_start_date/planned_end_date: ISO 8601 dates, ornullto clear. For work-order events these are applied to the work order. Settingplanned_start_datecascades the shift to successor dependencies.title: New title (up to 255 characters), ornullto clear. Ad-hoc events require a non-empty title, so it cannot be cleared for them.description: New description (up to 4096 characters), ornullor an empty string to clear.linked_entity_type/linked_entity_id: Change or clear the linked entity (ad-hoc events only). Provide both together.rrule: Set or change the recurrence rule.- With
apply_to_series, replaces the series' rule and regenerates its occurrences. Occurrences that have already started, are linked to a run, or have a link that differs from the series are kept; the rest are replaced by the new rule's future dates. Kept occurrences still receivetitleanddescriptionchanges, but keep their own links. - On a single event in a series, an empty string clears recurrence. Sending an empty string together with
apply_to_seriesis rejected. - On a non-recurring ad-hoc event, turns it into a series: the event becomes the first occurrence and the rule's remaining dates are created as new occurrences sharing its assignments. Requires
planned_start_dateand returns400if the event has dependencies, is not ad-hoc, or the rule produces no dates.
- With
apply_to_series: Whentrue, applies the update across the whole recurrence series. Mutually exclusive withdetach_from_series.detach_from_series: Whentrue, detaches this occurrence from its series. Mutually exclusive withapply_to_series.details: Up to 100 custom field values to set. Only the fields sent are changed; send{ "recorded": null }to clear a value. Withapply_to_series, the values are set on the series master and every occurrence. See Custom field values on events.
Owner, entity type, created_by, and assignments are not editable here. Manage assignments with the Assignments endpoints.
Custom field values on events
details on Create Events and Update Event is an array of objects with:
id: (required)idof the custom field definition (theidfrom List Custom Fields, not the user-definedfield_id). The field must be on the workspace's Schedule Event form.value.recorded: The value, in the shape Recorded Custom Field Values gives for the field'stype, ornull.
Date, Time and Date Time values may also be sent as ISO 8601 strings. They are stored as { date, time, zone } objects with zone set to UTC. A Date Time string with an offset is converted to UTC; a string without an offset is read as UTC.
The request returns 400 when a field is not on the form, when a value does not match the field's type, or when the event is a draft that belongs to a schedule plan. Required fields are not enforced by the API, so an event can be created or updated without them.
Example Response Body
{
"event": {
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"owner_entity_type": "project",
"owner_entity_id": "9NKegRHwzK",
"event_entity_type": "adhoc",
"planned_start_date": "2026-06-22T14:00:00.000Z",
"planned_end_date": "2026-06-22T15:30:00.000Z",
"entity_name": "Design review (rescheduled)",
"title": "Design review (rescheduled)",
"created_at": "2026-06-15T09:30:00.000Z",
"updated_at": "2026-06-15T10:05:00.000Z",
"created_by": "usr_7TqLmZ0pQ2",
"assignments": [],
"dependencies": [],
"recurrence_id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"is_recurrence_master": true,
"rrule": "FREQ=WEEKLY;COUNT=4"
}
}
Response
- Status:
200 OK - Body:
{ "event": { ... } }, the updated Event object. Withapply_to_series, this is the series master event.
Delete Event
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
params={"deleteSeries": "true"}
)
curl -X DELETE \
"https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d?deleteSeries=true" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d?deleteSeries=true"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers @{
"Authorization" = "Basic $authHeader"
}
DELETE https://api.epsilon3.io/v1/project-schedule/events/:eventId?deleteSeries={deleteSeries}
Delete an ad-hoc event. Only ad-hoc events can be deleted through this endpoint; entity-backed events (work orders, runs, etc.) cannot.
Parameters
eventId(path parameter): UUID of the ad-hoc event to delete.deleteSeries(query parameter): optional boolean (defaultfalse). Whentrueand the event belongs to a recurrence series, the entire series (master and all occurrences) is deleted.
Response
- Status:
204 No Content - Body: (empty)
Event Activity
Get Event Activity
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/activity',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
params={"startBeforeTimestamp": "2026-06-12T14:00:00.000Z"}
)
curl -X GET \
"https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/activity?startBeforeTimestamp=2026-06-12T14:00:00.000Z" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/activity?startBeforeTimestamp=2026-06-12T14:00:00.000Z"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
Invoke-RestMethod -Uri $apiUrl -Headers @{
"Authorization" = "Basic $authHeader"
}
GET https://api.epsilon3.io/v1/project-schedule/events/:eventId/activity?startBeforeTimestamp={startBeforeTimestamp}
List the activity history for an event, newest first. Up to 25 entries are returned per request.
Parameters
eventId(path parameter): UUID of the event whose activity is requested.startBeforeTimestamp(query parameter): optional ISO 8601 timestamp cursor. Only entries withcreated_atstrictly earlier than this value are returned. To page backward, pass thecreated_atof the oldest entry from the previous page.
Example Response Body
{
"older_count": 12,
"activity": [
{
"id": "3f1c2d4e-9a6b-4c1f-8e2a-7b5d9e0f1a23",
"entity_type": "project_schedule_event",
"entity_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"created_at": "2026-06-14T18:22:05.412Z",
"action": "Run Started",
"actor_type": "user",
"actor_id": "usr_7TqLmZ0pQ2",
"new_value": {
"linked_entity_type": "run",
"linked_entity_id": "aZ8kQ2mLpV"
}
},
{
"id": "b2a1c0d9-8e7f-4a3b-9c2d-1e0f5a4b3c2d",
"entity_type": "project_schedule_event",
"entity_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"created_at": "2026-06-13T09:10:33.001Z",
"action": "Planned Start and End Date Updated",
"actor_type": "api",
"old_value": {
"planned_start_date": "2026-06-20T08:00:00.000Z",
"planned_end_date": "2026-06-20T17:00:00.000Z"
},
"new_value": {
"planned_start_date": "2026-06-21T08:00:00.000Z",
"planned_end_date": "2026-06-21T17:00:00.000Z"
}
},
{
"id": "e5d4c3b2-1a0f-4e9d-8c7b-6a5f4e3d2c1b",
"entity_type": "project_schedule_event",
"entity_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"created_at": "2026-06-12T14:00:00.000Z",
"action": "Created",
"actor_type": "user",
"actor_id": "usr_7TqLmZ0pQ2"
}
]
}
Response
- Status:
200 OK - Body:
{ "older_count": number, "activity": [ ... ] }.older_countis the number of activity entries older than the oldest entry returned — a value greater than zero means more pages remain. Each activity entry contains:id: Activity id.entity_type: Alwaysproject_schedule_event.entity_id: The event id.created_at: ISO 8601 timestamp.updated_atis present only on edited entries (e.g. comments).action: The action that occurred, e.g.Created,Planned Start and End Date Updated,Planned Start Date Updated,Planned End Date Updated,Title Updated,Description Updated,Resources Updated,Procedure Linked,Run Linked,Run Started,Dependency Added,Dependency Removed,Dependency Type Updated,Custom Details Updated.actor_type:user,api, orrun.actor_idis present foruserandrunactors and absent forapiactors.old_value/new_value: Action-dependent detail objects (e.g. date changes carryplanned_start_date/planned_end_date; link changes carrylinked_entity_type/linked_entity_id).Custom Details Updatedhas only anew_valueof{ "details": [ ... ] }, the custom field values that were sent in the update. Withapply_to_series, each occurrence gets its own entry.comment,mention_list,attachments: Present for comment activity.
An event id with no activity returns { "older_count": 0, "activity": [] }.
Linking Runs
Link Run to Event
Sample Request
import requests
import os
data = {"run_id": "aZ8kQ2mLpV"}
requests.put(
url='https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/run',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PUT \
https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/run \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{"run_id": "aZ8kQ2mLpV"}'
$apiUrl = "https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/run"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{ run_id = "aZ8kQ2mLpV" } | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Put -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"run_id": "aZ8kQ2mLpV"
}
PUT https://api.epsilon3.io/v1/project-schedule/events/:eventId/run
Link an existing run to an ad-hoc event.
Parameters
eventId(path parameter): UUID of the ad-hoc event to link the run to.
Request Body
run_id: (required) Id of the run to link. The run must exist and be in a project the caller can access, or the request returns404. Stored as the event's linked entity (linked_entity_typebecomesrun).
Example Response Body
{
"event": {
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"owner_entity_type": "project",
"owner_entity_id": "9NKegRHwzK",
"event_entity_type": "adhoc",
"linked_entity_type": "run",
"linked_entity_id": "aZ8kQ2mLpV",
"planned_start_date": "2026-06-20T08:00:00.000Z",
"planned_end_date": "2026-06-20T17:00:00.000Z",
"actual_start_date": "2026-06-20T08:05:12.000Z",
"actual_end_date": null,
"entity_name": "Pre-flight Checkout",
"linked_run_data": {
"starttime": "2026-06-20T08:05:12.000Z",
"completedAt": null,
"code": "PFC-001",
"name": "Pre-flight Checkout",
"run_number": 7,
"state": "running",
"status": "running",
"total_steps": 24,
"completed_steps": 9,
"not_required_steps": 1,
"skipped_steps": 0,
"failed_steps": 0,
"project_id": "9NKegRHwzK"
},
"title": "Pre-flight Checkout",
"created_at": "2026-06-12T14:00:00.000Z",
"updated_at": "2026-06-20T08:05:30.000Z",
"created_by": "usr_7TqLmZ0pQ2",
"assignments": [],
"dependencies": [],
"is_recurrence_master": false
}
}
Response
- Status:
200 OK - Body:
{ "event": { ... } }, the updated Event object with the run linked. When the run is accessible to the caller,linked_run_datais populated.
Assignments
Assignments link a resource (user, tool, location, or user group) to an event with an allocation percentage. The endpoints in this group return the Event Assignment object, described once here.
Assigning a group is deprecated and will be removed. Assign the individual users instead.
The Event Assignment object
{
"id": "f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f",
"event_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"allocation_percent": 50,
"notes": "Lead engineer for this task",
"assigned_by": "usr_7TqLmZ0pQ2",
"created_at": "2026-06-15T14:32:10.123Z",
"resource": {
"type": "user",
"resource": {
"id": "9NKegRHwzK",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@example.com"
},
"name": "Ada Lovelace",
"iconImage": {
"name": "profile",
"content_type": "image/png",
"attachment_id": "img_5kQ9zRtW0a"
}
}
}
Event Assignment fields
id: Assignment id.event_id: Id of the event the assignment belongs to.allocation_percent: Integer 0–100.notes: Optional free-text notes (absent when not set).assigned_by: Id of the user that made the assignment.created_at: ISO 8601 timestamp.resource: The assigned resource, with:type:user,tool,location, orgroup.resource: The underlying resource record. For groups this is{ id, display_name, external_id, source }, wheredisplay_nameandexternal_idare absent when unset.name: A display name (user's name;<number> - <name> <serial>for tools;<code> <name>for locations; the group's display name, falling back to its external id then its id, for groups).iconImage: Present only for users with a profile image and tools with an image attachment.
Create Event Assignments
Sample Request
import requests
import os
data = {
"assignments": [
{
"resource_type": "user",
"resource_id": "9NKegRHwzK",
"allocation_percent": 50,
"assigned_by": "usr_7TqLmZ0pQ2",
"notes": "Lead engineer for this task"
},
{
"resource_type": "tool",
"resource_id": "482",
"allocation_percent": 100,
"assigned_by": "usr_7TqLmZ0pQ2"
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/assignments',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/assignments \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"assignments": [
{
"resource_type": "user",
"resource_id": "9NKegRHwzK",
"allocation_percent": 50,
"assigned_by": "usr_7TqLmZ0pQ2",
"notes": "Lead engineer for this task"
},
{
"resource_type": "tool",
"resource_id": "482",
"allocation_percent": 100,
"assigned_by": "usr_7TqLmZ0pQ2"
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/project-schedule/events/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/assignments"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
assignments = @(
@{
resource_type = "user"
resource_id = "9NKegRHwzK"
allocation_percent = 50
assigned_by = "usr_7TqLmZ0pQ2"
notes = "Lead engineer for this task"
},
@{
resource_type = "tool"
resource_id = "482"
allocation_percent = 100
assigned_by = "usr_7TqLmZ0pQ2"
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"assignments": [
{
"resource_type": "user",
"resource_id": "9NKegRHwzK",
"allocation_percent": 50,
"assigned_by": "usr_7TqLmZ0pQ2",
"notes": "Lead engineer for this task"
},
{
"resource_type": "tool",
"resource_id": "482",
"allocation_percent": 100,
"assigned_by": "usr_7TqLmZ0pQ2"
}
],
"apply_to_series": false
}
POST https://api.epsilon3.io/v1/project-schedule/events/:eventId/assignments
Add one or more resource assignments to an event.
Parameters
eventId(path parameter): UUID of the event to assign resources to.
Request Body
assignments: (required) Array of at least one assignment object. Each contains:resource_type: (required)user,tool,location, orgroup(deprecated).resource_id: (required) Id of the resource, which must exist. Tool and location ids are numeric strings; user ids are workspace user ids; group ids are UUIDs.allocation_percent: (required) Integer 0–100.assigned_by: (required) Id of the user making the assignment.notes: (optional) Up to 4096 characters.
apply_to_series: (optional) Whentrue, the same assignments are replicated to every other event in the recurrence series. Replicated assignments are not included in the response.
Assignments are upserted on (event_id, resource_type, resource_id): re-assigning a resource already on the event updates the existing assignment. The whole batch is atomic — a single missing resource fails the entire request.
Example Response Body
{
"assignments": [
{
"id": "f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f",
"event_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"allocation_percent": 50,
"notes": "Lead engineer for this task",
"assigned_by": "usr_7TqLmZ0pQ2",
"created_at": "2026-06-15T14:32:10.123Z",
"resource": {
"type": "user",
"resource": {
"id": "9NKegRHwzK",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@example.com"
},
"name": "Ada Lovelace"
}
},
{
"id": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
"event_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"allocation_percent": 100,
"assigned_by": "usr_7TqLmZ0pQ2",
"created_at": "2026-06-15T14:32:10.130Z",
"resource": {
"type": "tool",
"resource": {
"id": 482,
"tool_id": 17,
"serial_number": "SN-00482"
},
"name": "TRQ-100 - Torque Wrench SN-00482"
}
}
]
}
Response
- Status:
201 Created - Body:
{ "assignments": [ ... ] }, the Event Assignment objects created on the event.
Update Event Assignment
Sample Request
import requests
import os
data = {
"allocation_percent": 75,
"notes": "Reduced allocation after scope change",
"apply_to_series": True
}
requests.patch(
url='https://api.epsilon3.io/v1/project-schedule/events/assignments/f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/project-schedule/events/assignments/f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"allocation_percent": 75,
"notes": "Reduced allocation after scope change",
"apply_to_series": true
}'
$apiUrl = "https://api.epsilon3.io/v1/project-schedule/events/assignments/f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
allocation_percent = 75
notes = "Reduced allocation after scope change"
apply_to_series = $true
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"allocation_percent": 75,
"notes": "Reduced allocation after scope change",
"apply_to_series": true
}
PATCH https://api.epsilon3.io/v1/project-schedule/events/assignments/:assignmentId
Update a single assignment.
Parameters
assignmentId(path parameter): Id of the assignment to update.
Request Body
All fields are optional; supply at least one.
resource_id: Reassign to a different resource of the sameresource_type(the type cannot be changed). The new resource must exist.allocation_percent: Integer 0–100.notes: New notes (up to 4096 characters), ornullto clear.apply_to_series: Whentrue, the same update is applied to matching assignments (same originalresource_type+resource_id) on every other event in the series.
Example Response Body
{
"id": "f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f",
"event_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"allocation_percent": 75,
"notes": "Reduced allocation after scope change",
"assigned_by": "usr_7TqLmZ0pQ2",
"created_at": "2026-06-15T14:32:10.123Z",
"resource": {
"type": "user",
"resource": {
"id": "9NKegRHwzK",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@example.com"
},
"name": "Ada Lovelace"
}
}
Response
- Status:
200 OK - Body: A single Event Assignment object (not wrapped in an envelope).
Delete Event Assignments
Sample Request
import requests
import os
data = {"ids": ["f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f", "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d"]}
requests.delete(
url='https://api.epsilon3.io/v1/project-schedule/events/assignments',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X DELETE \
https://api.epsilon3.io/v1/project-schedule/events/assignments \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{"ids": ["f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f", "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d"]}'
$apiUrl = "https://api.epsilon3.io/v1/project-schedule/events/assignments"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{ ids = @("f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f", "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d") } | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"ids": ["f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f", "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d", "00000000-0000-4000-8000-000000000001"],
"apply_to_series": false
}
DELETE https://api.epsilon3.io/v1/project-schedule/events/assignments
Delete one or more assignments by id. Ids are sent in the request body.
Request Body
ids: (required) Array of 1 to 1000 assignment ids to delete. Ids that do not match an existing assignment are not errors — they are reported back ininvalid_item_ids.apply_to_series: (optional) Whentrue, the matching assignments are deleted from every event in the recurrence series. All ids must belong to the same event, otherwise the request returns400.
Example Response Body
{
"deleted_item_ids": ["f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f", "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d"],
"invalid_item_ids": ["00000000-0000-4000-8000-000000000001"]
}
Response
- Status:
200 OK - Body:
{ "deleted_item_ids": [ ... ], "invalid_item_ids": [ ... ] }.
Dependencies
Dependencies are directed links from a predecessor event to a successor event. Either endpoint of a dependency can be identified by an existing event id or by an entity reference, which finds or creates the underlying event. Creating, updating, or deleting a dependency requires edit access on the owning project of both events.
Create Event Dependencies
Sample Request
import requests
import os
data = {
"dependencies": [
{
"predecessor_event_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"successor_event_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"dependency_type": "finish_to_start",
"created_by": "usr_7TqLmZ0pQ2"
}
]
}
requests.post(
url='https://api.epsilon3.io/v1/project-schedule/events/dependencies',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X POST \
https://api.epsilon3.io/v1/project-schedule/events/dependencies \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"dependencies": [
{
"predecessor_event_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"successor_event_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"dependency_type": "finish_to_start",
"created_by": "usr_7TqLmZ0pQ2"
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/project-schedule/events/dependencies"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{
dependencies = @(
@{
predecessor_event_id = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
successor_event_id = "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
dependency_type = "finish_to_start"
created_by = "usr_7TqLmZ0pQ2"
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body (by event id and by entity reference)
{
"dependencies": [
{
"predecessor_event_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"successor_event_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"dependency_type": "finish_to_start",
"created_by": "usr_7TqLmZ0pQ2"
},
{
"predecessor_entity": {
"entity_type": "work_order",
"entity_id": "7QmTpRxY3z",
"owner_entity_type": "project",
"owner_entity_id": "9NKegRHwzK"
},
"successor_event_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"dependency_type": "start_to_start",
"created_by": "usr_7TqLmZ0pQ2"
}
]
}
POST https://api.epsilon3.io/v1/project-schedule/events/dependencies
Create one or more dependencies in a single atomic request.
Request Body
dependencies: (required) Non-empty array of dependency objects. Each contains:- A predecessor reference — exactly one of
predecessor_event_idorpredecessor_entity. - A successor reference — exactly one of
successor_event_idorsuccessor_entity. predecessor_entity/successor_entity: Entity reference of the form{ entity_type, entity_id, owner_entity_type, owner_entity_id }. If exactly one matching event exists it is reused; if none exists one is created; if more than one matches the request returns400.dependency_type: (optional) One offinish_to_start,start_to_start,finish_to_finish,start_to_finish. Defaults tofinish_to_start.created_by: (required) Id of the user the dependency is attributed to.
- A predecessor reference — exactly one of
The whole batch is validated and inserted in one transaction. A dependency that would create a cycle (against existing dependencies or others in the same batch) returns 400, as does any dependency that targets a recurring event.
Example Response Body
{
"ids": ["d1e2f3a4-b5c6-4d7e-8f90-1a2b3c4d5e6f", "e2f3a4b5-c6d7-4e8f-9a01-2b3c4d5e6f70"]
}
Response
- Status:
200 OK - Body:
{ "ids": [ ... ] }, the ids of the created dependencies in request order.
Update Event Dependency
Sample Request
import requests
import os
data = {"dependency_type": "start_to_start"}
requests.patch(
url='https://api.epsilon3.io/v1/project-schedule/events/dependencies/d1e2f3a4-b5c6-4d7e-8f90-1a2b3c4d5e6f',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X PATCH \
https://api.epsilon3.io/v1/project-schedule/events/dependencies/d1e2f3a4-b5c6-4d7e-8f90-1a2b3c4d5e6f \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{"dependency_type": "start_to_start"}'
$apiUrl = "https://api.epsilon3.io/v1/project-schedule/events/dependencies/d1e2f3a4-b5c6-4d7e-8f90-1a2b3c4d5e6f"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{ dependency_type = "start_to_start" } | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Response Body
{
"id": "d1e2f3a4-b5c6-4d7e-8f90-1a2b3c4d5e6f",
"predecessor_event_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"successor_event_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"dependency_type": "start_to_start",
"created_at": "2026-09-30T17:00:00.000Z",
"updated_at": "2026-09-30T18:00:00.000Z",
"created_by": "usr_7TqLmZ0pQ2"
}
PATCH https://api.epsilon3.io/v1/project-schedule/events/dependencies/:dependencyId
Change the type of an existing dependency. The events' planned dates are not changed; the new type applies to future date shifts of the predecessor.
Request Body
dependency_type: (required) One offinish_to_start,start_to_start,finish_to_finish,start_to_finish.
A dependency id that does not exist returns 404. Changing the type to one that already links the same two events returns 400.
Response
- Status:
200 OK - Body: The updated dependency.
Delete Event Dependencies
Sample Request
import requests
import os
data = {"dependency_ids": ["d1e2f3a4-b5c6-4d7e-8f90-1a2b3c4d5e6f"]}
requests.delete(
url='https://api.epsilon3.io/v1/project-schedule/events/dependencies',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json=data
)
curl -X DELETE \
https://api.epsilon3.io/v1/project-schedule/events/dependencies \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{"dependency_ids": ["d1e2f3a4-b5c6-4d7e-8f90-1a2b3c4d5e6f"]}'
$apiUrl = "https://api.epsilon3.io/v1/project-schedule/events/dependencies"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$body = @{ dependency_ids = @("d1e2f3a4-b5c6-4d7e-8f90-1a2b3c4d5e6f") } | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
} -Body $body
Example Request Body
{
"dependency_ids": ["d1e2f3a4-b5c6-4d7e-8f90-1a2b3c4d5e6f", "00000000-0000-4000-8000-000000000009"]
}
DELETE https://api.epsilon3.io/v1/project-schedule/events/dependencies
Delete one or more dependencies by id. Ids are sent in the request body.
Request Body
dependency_ids: (required) Non-empty array of dependency ids to delete. Ids that do not match an existing dependency are reported back ininvalid_idsrather than causing an error.
Example Response Body
{
"deleted_ids": ["d1e2f3a4-b5c6-4d7e-8f90-1a2b3c4d5e6f"],
"invalid_ids": ["00000000-0000-4000-8000-000000000009"]
}
Response
- Status:
200 OK - Body:
{ "deleted_ids": [ ... ], "invalid_ids": [ ... ] }.
Roles API
Supports managing roles defined in your workspace. Roles control user permissions and can have required skills associated with them.
List Roles
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v2/roles',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v2/roles \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v2/roles"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v2/roles
Retrieve all roles defined in your workspace, including their permissions and required skills.
Example Response Body
{
"roles": [
{
"id": "1001",
"name": "Admin",
"code": "ADMIN",
"is_paid": true,
"permissions": [
{
"key": "settings_admin",
"label": "Settings Admin",
"is_paid": true
},
{
"key": "run_procedures",
"label": "Run Procedures",
"is_paid": false
}
],
"required_skills": [
{
"id": "2001",
"skill_name": "IFR Instrument Proficiency Check",
"archived": false
}
]
}
]
}
Get a Role
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v2/roles/{role_id}',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v2/roles/{role_id} \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v2/roles/{role_id}"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v2/roles/{role_id}
Retrieve a single role by its ID, including its permissions and required skills.
Example Response Body
{
"id": "1001",
"name": "Admin",
"code": "ADMIN",
"is_paid": true,
"permissions": [
{
"key": "settings:admin",
"label": "Settings Admin",
"is_paid": true
}
],
"required_skills": [
{
"id": "2001",
"skill_name": "IFR Instrument Proficiency Check",
"archived": false
}
]
}
Create a Role
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v2/roles',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
'name': 'Flight Engineer',
'code': 'FLIGHT_ENG',
'permissions': ['procedures:view', 'runs:operate'],
'required_skill_ids': ['2001']
}
)
curl -X POST \
https://api.epsilon3.io/v2/roles \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"name": "Flight Engineer",
"code": "FLIGHT_ENG",
"permissions": ["procedures:view", "runs:operate"],
"required_skill_ids": ["2001"]
}'
$apiUrl = "https://api.epsilon3.io/v2/roles"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
}
$body = @{
name = "Flight Engineer"
code = "FLIGHT_ENG"
permissions = @("procedures:view", "runs:operate")
required_skill_ids = @("2001")
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
POST https://api.epsilon3.io/v2/roles
Create a new role in your workspace.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Display name of the role. |
code |
string | Yes | Unique short code for the role within the workspace. |
permissions |
array of strings | Yes | Permission keys granted to the role. |
required_skill_ids |
array of strings | No | IDs of skills required for the role. |
Example Response Body
{
"id": "1001"
}
Update a Role
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v2/roles/{role_id}',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
'name': 'Senior Flight Engineer',
'permissions': ['procedures:view', 'runs:operate', 'settings:admin']
}
)
curl -X PATCH \
https://api.epsilon3.io/v2/roles/{role_id} \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{
"name": "Senior Flight Engineer",
"permissions": ["procedures:view", "runs:operate", "settings:admin"]
}'
$apiUrl = "https://api.epsilon3.io/v2/roles/{role_id}"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
}
$body = @{
name = "Senior Flight Engineer"
permissions = @("procedures:view", "runs:operate", "settings:admin")
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
PATCH https://api.epsilon3.io/v2/roles/{role_id}
Update an existing role. This is a partial update: only the fields you include are changed, and any field you omit is left unchanged. At least one field must be provided. Omitting required_skill_ids leaves the role's skills untouched, while providing it replaces the full set.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | No | Display name of the role. |
code |
string | No | Unique short code for the role within the workspace. |
permissions |
array of strings | No | Permission keys granted to the role. Replaces the existing set. |
required_skill_ids |
array of strings | No | IDs of skills required for the role. Replaces the existing set. |
A successful update returns 204 No Content.
Delete a Role
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v2/roles/{role_id}',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X DELETE \
https://api.epsilon3.io/v2/roles/{role_id} \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v2/roles/{role_id}"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers $headers
DELETE https://api.epsilon3.io/v2/roles/{role_id}
Delete a role from your workspace. A successful delete returns 204 No Content.
Runs V2 API
Get Run
Retrieve a single run using its ID.
HTTP Request
GET https://api.epsilon3.io/v2/runs/:runId
Path Parameters
runId(string): ID of the run being retrieved.
Query Parameters
label-style(optional string):letters(A,A1,A1.1) ornumbers(1,1.1,1.1.1). Overrides the workspace's Section Labels Displayed As setting for this request. Defaults to that setting.weight-unit(optional string):g,kg,ozorlb. The unit for the recorded weights in Builds blocks. Defaults to the workspace's small unit (gfor metric,ozfor imperial), orgwhen the workspace does not have the Weight Units module. See Weight Units.
Sample Request
import requests
import os
runId = 'lkC6ZrPrvPnPU4OAqNYcW4'
api_url = f"{os.environ.get('API_URL')}/v2/runs/{runId}"
response = requests.get(
url=api_url,
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
print(response.json())
curl -X GET \
https://api.epsilon3.io/v2/runs/{runId} \
-u "$EPSILON3_API_KEY":
$runId = "lkC6ZrPrvPnPU4OAqNYcW4"
$apiUrl = "https://api.epsilon3.io/v2/runs/$runId"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
Example Request URL
GET https://api.epsilon3.io/v2/runs/lkC6ZrPrvPnPU4OAqNYcW4
Example Response Body
{
"_id": string,
"_rev": (optional) string,
"procedure_rev": string,
"name": string,
"start_time": string,
"edited_at": (optional) string,
"edited_user_id": (optional) string,
"procedure_id": (optional) string,
"run_number": (optional) number,
"project_id": (optional) string,
"headers": (optional) Array<RunHeader>,
"code": string,
"variables": (optional) Array<RunVariable>,
"risks": (optional) Array<ProcedureRisk>,
"bill_of_materials": (optional) BillOfMaterials,
"bom_view": (optional) RunBomView,
"version": (optional) string,
"description": string,
"owner": (optional) string,
"state": (optional) RunState,
"status": (optional) RunStatus,
"sections": Array<RunSection | RepeatedSection>,
"participants": (optional) Array<Participant>,
"archived": (optional) boolean,
"actions": (optional) Array<RunAction>,
"reviewer_actions_history": (optional) Array<ReviewerAction>,
"reviewer_groups": (optional) Array<ReviewerGroup>,
"operation": (optional) OperationId,
"run_tags": (optional) Array<RunTag>,
"run_section": (optional) string,
"completedAt": (optional) string,
"completedUserId": (optional) string,
"procedure_rev_num": (optional) number,
"approvals": (optional) Array<Approval>,
"tags": (optional) Array<Tag>,
"settings": (optional) Settings,
"reviewers": (optional) Array<Reviewer>,
"default_view_mode": (optional) string,
"source_run": (optional) string,
"parent_reference": (optional) RunParentReference,
"started_by": (optional) {
"method": string,
"user_id": string
},
"issues": (optional) Record<string, RunIssue>,
"procedure": (optional) string,
"end_run_signoffs_groups": (optional) Array<EndRunSignoff>,
"start_run_signoffs_groups": (optional) Array<StartRunSignoff>,
"dictionary_id": (optional) string,
"comments": (optional) Array<ReviewComment>,
"operators": (optional) Array<string>,
"procedure_type": (optional) ProcedureType,
"automation_status": (optional) AutomationStatus,
"test_case_list": (optional) TestCaseList,
"auto_procedure_id_enabled": (optional) boolean,
"details": (optional) Array<CustomFieldValue>,
"procedure_details": (optional) Array<CustomFieldValue>
}
details and procedure_details carry the run-level and procedure-level custom field values respectively. Both arrays include an entry for every custom field added to the procedure's header, even when no value has been recorded. See the Custom Fields API for the full CustomFieldValue wire shape.
RunHeader
{
"id": string,
"name": string,
"content": Array<ContentBlock>,
"redlines": (optional) Array<RunHeaderRedline>
}
RunVariable
{
"name": string,
"input_type": (optional) string,
"value": (optional) string | number | boolean
}
ProcedureRisk
{
"id": number,
"title": string
}
RunSection
{
"id": string,
"name": string,
"display_label": (optional) string,
"headers": (optional) Array<RunSectionHeader>,
"steps": Array<RunStep | RunAddedStep | RepeatedStep>,
"dependencies": (optional) Array<Dependency>,
"snippet_id": (optional) string,
"snippet_name": (optional) string,
"snippet_rev": (optional) number
}
RepeatedSection
{
"id": string,
"name": string,
"display_label": (optional) string,
"headers": (optional) Array<RunSectionHeader>,
"steps": Array<RunStep | RepeatedStep>,
"dependencies": (optional) Array<Dependency>,
"snippet_id": (optional) string,
"snippet_name": (optional) string,
"snippet_rev": (optional) number,
"repeat_of": string,
"repeated_at": string,
"repeated_user_id": string
}
RunStep
{
"id": string,
"run_id": (optional) string,
"name": string,
"display_label": (optional) string,
"type": (optional) string,
"state": (optional) "failed" | "paused",
"headers": (optional) Array<RunStepHeader>,
"content": Array<RunStepBlock>,
"dependencies": (optional) Array<Dependency>,
"signoffs": Array<Signoff>,
"conditionals": (optional) Array<Conditional>,
"actions": (optional) Array<StepAction>,
"completed": (optional) true,
"completed_at": (optional) string,
"completed_user_id": (optional) string,
"skipped": (optional) true,
"skipped_at": (optional) string,
"skipped_user_id": (optional) string,
"snippet_id": (optional) string,
"snippet_name": (optional) string,
"snippet_rev": (optional) number,
"timing": (optional) string,
"timer": (optional) StepTiming,
"duration": (optional) StepDuration,
"expected_duration": (optional) string,
"location": (optional) string,
"channel": (optional) string,
"redlines": (optional) Array<RunStepRedline>,
"comments": (optional) Array<RunStepComment>,
"shows_snippet_detached_message": (optional) boolean,
"preceding_step_id": (optional) string,
"automation_status": (optional) AutomationStatus,
"batch_props": (optional) {
"batch_size": number,
"index": number,
"batch_id": string,
"instance_id": string,
"label": (optional) string
}
}
RunAddedStep
{
"id": string,
"name": string,
"display_label": (optional) string,
"content": Array<RunStepBlock>,
"signoffs": Array<Signoff>,
"dependencies": (optional) Array<Dependency>,
"conditionals": (optional) Array<Conditional>,
"actions": (optional) Array<StepAction>,
"completed": (optional) true,
"completed_at": (optional) string,
"completed_user_id": (optional) string,
"skipped": (optional) true,
"skipped_at": (optional) string,
"skipped_user_id": (optional) string,
"snippet_id": (optional) string,
"snippet_name": (optional) string,
"snippet_rev": (optional) number,
"timing": (optional) string,
"timer": (optional) StepTiming,
"duration": (optional) StepDuration,
"expected_duration": (optional) string,
"location": (optional) string,
"channel": (optional) string,
"created_during_run": true,
"created_at": string,
"created_by": string
}
Participant
{
"user_id": string,
"created_at": string,
"type": string
}
RunAction
{
"timestamp": string,
"user_id": string,
"type": "pause" | "resume" | "change operation" | "reopen",
"comment": (optional) string
}
EndRunSignoff / StartRunSignoff
{
"id": string,
"operators": Array<string>
}
Tag
{
"id": string,
"name": string
}
Settings
{
"print_settings": (optional) {
"header": (optional) {
"text": (optional) string,
"variable_id": (optional) string
},
"footer": (optional) {
"text": (optional) string,
"variable_id": (optional) string
}
}
}
RunParentReference
{
"id": string | number,
"type": "run" | "procedure" | "issue" | "risk" | "event" | "operation" | "project" | "work_order" | "tool_instance" | "item" | "location" | "shipment" | "test_plan" | "part"
}
parent_reference records what the run was started from, such as a schedule event, an issue, a work order, or a part. type names the kind of entity and id is that entity's ID, with two exceptions:
operation:idis the operation key.tool_instance:idis"<tool_id>:<tool_instance_id>".
A run started from another run records that run's ID in source_run instead of setting parent_reference.
RunIssue
{
"id": string | number,
"text": string,
"url": (optional) string,
"type": "Jira" | "Internal",
"severity": (optional) Severity,
"status": (optional) Status
}
AutomationStatus
{
"status": string,
"message": (optional) string
}
Signoff
{
"id": string,
"operators": Array<string>
}
Dependency
{
"id": string,
"dependent_ids": Array<string>
}
Conditional
{
"id": string,
"source_type": "step" | "content" | "content_binary" | "content_ternary" | "duration",
"state": string,
"content_id": (optional) string,
"source_id": string,
"target_id": string,
"target_type": "step"
}
RunStepHeader
{
"id": string,
"name": string,
"content": Array<RunStepBlock>
}
RunStepComment
{
"text": string,
"timestamp": string,
"updated_at": (optional) string,
"user": string,
"id": string,
"parent_id": string,
"attachment": (optional) object,
"mention_list": (optional) Array<Mention>,
"reference_id": (optional) string
}
StepAction
{
"type": "signoff" | "revoke_signoff" | "complete" | "fail" | "skip" | "issue",
"user_id": string,
"timestamp": string,
"comment": (optional) string,
"signoff_id": (optional) string,
"operator": (optional) string,
"device_user_id": (optional) string
}
RunStepBlock
{
"type": string,
"content_id": string,
"display_label": (optional) string,
"details": (optional) object,
"metadata": (optional) object
}
The RunStepBlock object varies by type. Below are the schemas for the builds content block types.
InventoryCheckout (
type: "inventory_checkout")
{
"id": string,
"type": "inventory_checkout",
"parts": [
{
"part_id": string,
"part_revision_id": string,
"quantity": number,
"details": Array<CustomFieldValue>
}
],
"items": (nullable) [
{
"item_id": (nullable) string,
"part_id": string,
"part_revision_id": string,
"quantity": number
}
],
"include_in_summary": boolean
}
Each entry in parts carries the workspace's custom field column values for that row in details. The array is empty when no columns are configured for the block. See Recorded Custom Field Values for the CustomFieldValue wire shape.
InventoryCheckin (
type: "inventory_checkin")
{
"id": string,
"type": "inventory_checkin",
"parts": [
{
"part_id": string,
"part_revision_id": string,
"quantity": number,
"details": Array<CustomFieldValue>
}
],
"items": (nullable) [
{
"item_id": (nullable) string,
"part_id": string,
"part_revision_id": string,
"quantity": number
}
],
"include_in_summary": boolean
}
details on each entry in parts behaves the same as for InventoryCheckout.
KitInventory (
type: "kit_inventory")
{
"id": string,
"type": "kit_inventory",
"assembly_part_id": string,
"assembly_part_revision_id": string,
"parts": [
{
"part_id": string,
"part_revision_id": string,
"quantity": number
}
],
"items": (nullable) [
{
"item_id": (nullable) string,
"part_id": string,
"part_revision_id": string,
"quantity": number
}
],
"kit_display_id": (nullable) string,
"location_id": (nullable) number,
"include_in_summary": boolean
}
AddInventory (
type: "add_inventory")
{
"id": string,
"type": "add_inventory",
"parts": [
{
"part_id": string,
"part_revision_id": (nullable) string,
"quantity": number
}
],
"items": (nullable) [
{
"item_id": (nullable) string,
"part_id": string,
"part_revision_id": (nullable) string,
"quantity": number,
"components": [
{
"item_id": string,
"part_id": string,
"part_revision_id": string,
"quantity": number,
"unit_weight": (nullable) number,
"weight_unit": "g" | "kg" | "oz" | "lb",
"unit_weight_grams": (nullable) number
}
]
}
],
"include_in_summary": boolean,
"record_weight": boolean
}
unit_weight is the per-unit weight the operator entered for an installed component, in weight_unit. It is null when the block does not record weight (record_weight: false) or no weight was entered. unit_weight_grams is deprecated: it is the same weight in grams. Use unit_weight instead.
DeleteInventoryKit (
type: "delete_inventory_kit")
{
"id": string,
"type": "delete_inventory_kit",
"assembly_part_id": string,
"assembly_part_revision_id": string,
"kit_id": (nullable) string,
"kit_display_id": (nullable) string,
"kit_components": (nullable) [
{
"item_id": string,
"quantity": number
}
],
"include_in_summary": boolean
}
UpdateComponents (
type: "update_components")
{
"id": string,
"type": "update_components",
"assembly_part_id": (nullable) string,
"assembly_part_revision_id": (nullable) string,
"item_id": (nullable) string,
"removed": (nullable) [
{
"item_id": string,
"ref_des_group_id": (nullable) string,
"quantity": number,
"swap_enabled": boolean,
"swap_item_id": (nullable) string,
"swap_unit_weight": (nullable) number,
"weight_unit": "g" | "kg" | "oz" | "lb",
"swap_unit_weight_grams": (nullable) number
}
],
"added": (nullable) [
{
"part_id": string,
"part_revision_id": string,
"item_id": (nullable) string,
"ref_des_group_id": (nullable) string,
"quantity": number,
"kit_id": (nullable) string,
"kit_display_id": (nullable) string,
"unit_weight": (nullable) number,
"weight_unit": "g" | "kg" | "oz" | "lb",
"unit_weight_grams": (nullable) number
}
],
"kit_ids": (nullable) [string],
"include_in_summary": boolean,
"access_mode": "all" | "restrict",
"record_weight": boolean,
"defined_components": [
{
"part_id": string,
"part_revision_id": string,
"ref_des_group_id": (nullable) string,
"update_type": "add" | "remove" | "swap",
"quantity": number,
"details": Array<CustomFieldValue>
}
]
}
unit_weight and swap_unit_weight are the per-unit weights the operator entered for an added or swapped-in component, in weight_unit. They are null when the block does not record weight (record_weight: false) or no weight was entered. unit_weight_grams and swap_unit_weight_grams are deprecated: they are the same weights in grams. Use unit_weight and swap_unit_weight instead.
defined_components lists the components a restricted (access_mode: "restrict") block allows operators to add, remove, or swap. details carries the workspace's custom field column values for each defined component. The array is empty when no columns are configured for the block. See Recorded Custom Field Values for the CustomFieldValue wire shape.
Tir (
type: "tir")
{
"id": string,
"type": "tir",
"parent_part_id": string,
"parent_part_revision_id": string,
"parent_item_id": (nullable) string,
"access_mode": "all" | "restrict",
"defined_installations": [
{
"part_id": (nullable) string,
"part_revision_id": (nullable) string,
"tool_id": (nullable) number,
"update_type": "install" | "remove",
"quantity": number
}
],
"installed": (nullable) [
{
"part_id": (nullable) string,
"part_revision_id": (nullable) string,
"tool_id": (nullable) number,
"installation_item_id": (nullable) string,
"installation_tool_instance_id": (nullable) number,
"name": (nullable) string,
"quantity": number,
"weight": (nullable) number,
"weight_unit": "g" | "kg" | "oz" | "lb",
"weight_grams": (nullable) number,
"description": (nullable) string,
"tir_number": (nullable) string
}
],
"removed": (nullable) [
{
"tir_log_event_id": string,
"status": "removed" | "lost" | "already_removed" | "waived" | "voided",
"quantity": (nullable) number,
"description": (nullable) string
}
],
"require_photo": boolean,
"include_in_summary": boolean
}
weight is the weight the operator entered for an installed entry, in weight_unit. It is null when no weight was entered. weight_grams is deprecated: it is the same weight in grams. Use weight instead.
RecordUsage (
type: "record_usage")
{
"id": string,
"type": "record_usage",
"access_mode": "all" | "restrict",
"entry_mode": "delta" | "current_reading",
"rows": [
{
"row_id": string,
"subject_kind": "part" | "tool" | "location",
"part_id": (nullable) string,
"part_revision_id": (nullable) string,
"tool_id": (nullable) number,
"location_id": (nullable) number,
"usage_type_id": (nullable) number
}
],
"entries": (nullable) [
{
"row_id": string,
"subject_kind": (nullable) "part" | "tool" | "location",
"part_id": (nullable) string,
"part_revision_id": (nullable) string,
"tool_id": (nullable) number,
"location_id": (nullable) number,
"usage_type_id": (nullable) number,
"item_id": (nullable) string,
"tool_instance_id": (nullable) number,
"value": (nullable) number
}
],
"apply_to_components": boolean,
"include_in_summary": boolean
}
BomView (
type: "bom_view")
{
"id": string,
"type": "bom_view",
"assembly_part_id": string,
"assembly_part_revision_id": string
}
BillOfMaterials
{
"part": {
"part_id": (optional) string,
"revision_id": (optional) string
},
"components": [
{
"part_id": (optional) string,
"revision_id": (optional) string,
"quantity": (optional) number
}
]
}
RunBomView
{
"assembly_part_id": string,
"assembly_part_revision_id": string
}
Runs API
Epsilon3’s Runs API supports creating and managing runs and associated data.
Start a running procedure
This endpoint creates new procedure runs.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/runs',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"procedure_id": "up9goX1AgzCuffxUXbpI1K",
"procedure_variables": {
"manufacturer": "tesla",
"VIN": 12345,
"four-wheel drive?": false,
}
}
)
curl -X POST \
https://api.epsilon3.io/v1/runs \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"procedure_id": "up9goX1AgzCuffxUXbpI1K",
"procedure_variables": {
"manufacturer": "tesla",
"VIN": 12345,
"four-wheel drive?": false,
}
}'
$apiUrl = "https://api.epsilon3.io/v1/runs"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"procedure_id": "up9goX1AgzCuffxUXbpI1K",
"procedure_variables": {
"manufacturer": "tesla",
"VIN": 12345,
"four-wheel drive?": false,
}
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"procedure_id": string,
"procedure_variables": {
"variable name": string | number | boolean
}
}
POST https://api.epsilon3.io/v1/runs
Example Response Body
{
"run_id": "GMFvqA1qLgrw0xIEc95M2o",
"run_number": 42
}
The request body must contain the id of the procedure to start, and can optionally be used to pre-populate procedure variables.
Request value types correspond to the type of the procedure variable:
| Procedure Variable Type | Request Value Type |
|---|---|
| Text | string |
| Number | number; or, a string parsable to a number |
| Checkbox | boolean |
| List & Custom List | string |
| Multiple Choice | string |
| Timestamp | string, timestamp in ISO-8601 format* |
*For time-only timestamp variables (with no date component required), the format may be HH:mm or HH:mm:ss. The date portion will default to the start of the epoch - January 1, 1970 UTC
Unsupported variable types: Attachment, Sketch, External Data, External Search
If no procedure exists with the given id, the response code will be 404. A procedure that is a reference document, is not released, or is archived cannot be run, and the response code will be 409.
The run can be viewed at https://app.epsilon3.io/runs/<run_id>.
Pause a running procedure
This endpoint allows you to pause a running procedure with an optional comment and a pause reason that is required only when team settings require a pause reason. The pause can also carry release constraints that must be met before the run is resumed.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/pause',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"comment": "Safety check pause",
"pause_reason_id": "aB3dEf7hIjKlMnOpQrStUv",
"resume_mode": "auto",
"constraints": [
{ "type": "issue", "issue_id": 482, "status_condition": { "operator": "in", "ids": [3] } }
]
}
)
curl -X POST \
https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/pause \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"comment": "Safety check pause",
"pause_reason_id": "aB3dEf7hIjKlMnOpQrStUv",
"resume_mode": "auto",
"constraints": [
{ "type": "issue", "issue_id": 482, "status_condition": { "operator": "in", "ids": [3] } }
]
}'
$apiUrl = "https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/pause"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"comment" = "Safety check pause"
"pause_reason_id" = "aB3dEf7hIjKlMnOpQrStUv"
"resume_mode" = "auto"
"constraints" = @(
@{
"type" = "issue"
"issue_id" = 482
"status_condition" = @{ "operator" = "in"; "ids" = @(3) }
}
)
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"comment": string (optional),
"pause_reason_id": string (optional),
"resume_mode": string (optional),
"constraints": array (optional)
}
POST https://api.epsilon3.io/v1/runs/:runId/pause
The runId parameter in the URL must contain the id of the running procedure to pause.
| Field | Value |
|---|---|
| comment | String (optional) - a note describing why the run was paused |
| pause_reason_id | String (optional, required with setting) - the id of a pause reason configured in your team's run settings, retrieved from GET /v1/settings/pause-reasons |
| resume_mode | String (optional) - manual (default) or auto. Decides what happens once every release constraint is met. See Release constraints below |
| constraints | Array (optional) - release constraints the run must clear before it can be resumed. See Release constraints below |
Release constraints
A pause on a run, section, or step can name things outside that run which must happen before it is resumed: an issue reaching a status or severity, another run ending, or the next run of a procedure ending. While any constraint is unmet, the matching resume endpoint responds 400. Every constraint on a pause must be met, each issue, run, or procedure may appear in at most one constraint, and a request may carry up to 50.
Example constraints
"resume_mode": "auto",
"constraints": [
{
"type": "issue",
"issue_id": 482,
"status_condition": { "operator": "in", "ids": [3] },
"severity_condition": { "operator": "in", "ids": [4] }
},
{
"type": "run",
"run_id": "up9goX1AgzCuffxUXbpI1K"
},
{
"type": "procedure",
"procedure_id": "GMFvqA1qLgrw0xIEc95M2o"
}
]
| Field | Value |
|---|---|
| type | String - issue to wait on an issue, run to wait on another run, procedure to wait on the next run of a procedure |
| issue_id | Integer (issue constraints) - the id of the issue to wait on, as returned by GET /v1/issues |
| status_condition | Object (issue constraints, optional) - { "operator": "in" or "not_in", "ids": [integer] }. Met once the issue's status is in (in) or outside (not_in) the listed ids. Status ids come from GET /v1/issues/metadata-options; up to 100 per condition |
| severity_condition | Object (issue constraints, optional) - the same shape, judged against the issue's severity. Severity ids come from GET /v1/issues/metadata-options |
| run_id | String (run constraints) - the id of another run. Met once that run ends |
| procedure_id | String (procedure constraints) - the id of a released procedure. Met when the first run of that procedure to start after the pause is applied has ended. Runs already under way when the pause is applied do not count; use a run constraint to wait on one of those |
An issue constraint needs at least one condition; when it carries both, either one being met releases it. Because either one releases it, a broad not_in is usually met already — including when the issue has no severity set — and a constraint that is already met is rejected with 400. A constraint is checked when the pause is requested, and later whenever the issue it names is updated or the run it names ends. A procedure constraint is checked again when a run of that procedure starts, which fixes the run it waits on, and once more when that run ends.
resume_mode decides what happens once every constraint is met:
| Value | Behavior |
|---|---|
| manual | Default. The run, section, or step stays paused, but its resume endpoint now succeeds. A holds-released action is recorded so operators can see the pause is ready to resume |
| auto | The run, section, or step resumes on its own, with a resume action noting that all pause constraints were met |
Resume a paused run
This endpoint allows you to resume a paused procedure with an optional comment.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/resume',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={ "comment": "Resuming after safety check" }
)
curl -X POST \
https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/resume \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{ "comment": "Resuming after safety check" }'
$apiUrl = "https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/resume"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{ "comment": "Resuming after safety check" } | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{ "comment": string (optional) }
POST https://api.epsilon3.io/v1/runs/:runId/resume
The runId parameter in the URL must contain the id of the paused run to resume.
| Field | Value |
|---|---|
| comment | String (optional) - a note describing why the run was resumed |
Pause a section
This endpoint allows you to pause a single section of a running procedure with an optional comment and a pause reason that is required only when team settings require a pause reason. The run itself keeps running, so work in its other sections continues. The pause can also carry release constraints that must be met before the section is resumed.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/sections/sctn_zk5SU0lzbF/pause',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={ "comment": "Waiting on the pad crew", "pause_reason_id": "aB3dEf7hIjKlMnOpQrStUv" }
)
curl -X POST \
https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/sections/sctn_zk5SU0lzbF/pause \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{ "comment": "Waiting on the pad crew", "pause_reason_id": "aB3dEf7hIjKlMnOpQrStUv" }'
$apiUrl = "https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/sections/sctn_zk5SU0lzbF/pause"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{ "comment": "Waiting on the pad crew"; "pause_reason_id": "aB3dEf7hIjKlMnOpQrStUv" } | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"comment": string (optional),
"pause_reason_id": string (optional),
"resume_mode": string (optional),
"constraints": array (optional)
}
POST https://api.epsilon3.io/v1/runs/:runId/sections/:sectionId/pause
The runId parameter in the URL must contain the id of the running procedure, and the sectionId parameter the id of the section to pause. Both are returned by GET /v1/runs/:runId.
| Field | Value |
|---|---|
| comment | String (optional) - a note describing why the section was paused, recorded on the pause. It is also posted as a run chat message when the request is authenticated as a user, since a chat message needs an author |
| pause_reason_id | String (optional, required with setting) - the id of a pause reason configured in your team's run settings |
| resume_mode | String (optional) - manual (default) or auto. Decides what happens once every release constraint is met. See Release constraints below |
| constraints | Array (optional) - release constraints the section must clear before it can be resumed. See Release constraints below |
Resume a paused section
This endpoint allows you to resume a paused section.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/sections/sctn_zk5SU0lzbF/resume',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={}
)
curl -X POST \
https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/sections/sctn_zk5SU0lzbF/resume \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{}'
$apiUrl = "https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/sections/sctn_zk5SU0lzbF/resume"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{}
POST https://api.epsilon3.io/v1/runs/:runId/sections/:sectionId/resume
The runId parameter in the URL must contain the id of the running procedure, and the sectionId parameter the id of the paused section to resume. This endpoint takes no request body.
Pause a step
This endpoint allows you to pause a single step of a running procedure with an optional comment and a pause reason that is required only when team settings require a pause reason. The run and its other steps keep going, so this holds only the step you name. The pause can also carry release constraints that must be met before the step is resumed.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/steps/step_Gi2nyYKZjf/pause',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={ "comment": "Holding for a torque check", "pause_reason_id": "aB3dEf7hIjKlMnOpQrStUv" }
)
curl -X POST \
https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/steps/step_Gi2nyYKZjf/pause \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{ "comment": "Holding for a torque check", "pause_reason_id": "aB3dEf7hIjKlMnOpQrStUv" }'
$apiUrl = "https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/steps/step_Gi2nyYKZjf/pause"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{ "comment": "Holding for a torque check"; "pause_reason_id": "aB3dEf7hIjKlMnOpQrStUv" } | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"comment": string (optional),
"pause_reason_id": string (optional),
"resume_mode": string (optional),
"constraints": array (optional)
}
POST https://api.epsilon3.io/v1/runs/:runId/steps/:stepId/pause
The runId parameter in the URL must contain the id of the running procedure, and the stepId parameter the id of the step to pause. Both are returned by GET /v1/runs/:runId. The step's section is looked up for you, so you do not need to send it.
| Field | Value |
|---|---|
| comment | String (optional) - a note describing why the step was paused, recorded as a comment on the step |
| pause_reason_id | String (optional, required with setting) - the id of a pause reason configured in your team's run settings |
| resume_mode | String (optional) - manual (default) or auto. Decides what happens once every release constraint is met. See Release constraints below |
| constraints | Array (optional) - release constraints the step must clear before it can be resumed. See Release constraints below |
Resume a paused step
This endpoint allows you to resume a paused step with an optional comment. It releases the hold whether an operator applied it in the app or a caller applied it through the API.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/steps/step_Gi2nyYKZjf/resume',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={ "comment": "Torque check complete" }
)
curl -X POST \
https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/steps/step_Gi2nyYKZjf/resume \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{ "comment": "Torque check complete" }'
$apiUrl = "https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/steps/step_Gi2nyYKZjf/resume"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{ "comment": "Torque check complete" } | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{ "comment": string (optional) }
POST https://api.epsilon3.io/v1/runs/:runId/steps/:stepId/resume
The runId parameter in the URL must contain the id of the running procedure, and the stepId parameter the id of the paused step to resume.
| Field | Value |
|---|---|
| comment | String (optional) - a note describing why the step was resumed, recorded as a comment on the step |
Retrieve list of running procedure summaries
This endpoint allows you to retrieve a list of summarized running procedures with an optional filter for run state.
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/runs/summary',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
params={
"run-state": ["paused", "completed"],
}
)
curl -X GET \
"https://api.epsilon3.io/v1/runs/summary?run-state=paused&run-state=completed" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/runs/summary?run-state=paused&run-state=completed"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/runs/summary
| Query parameter | Value |
|---|---|
| run-state | Array - running | paused | completed |
| operation-keys | Array - string |
| run-ids | Array - string (1 to 50 run ids) |
| project-ids | Array - string |
| tag-ids | Array - string |
| procedure-ids | Array - string |
| start-time-before | String - ISO8601 datetime |
| start-time-after | String - ISO8601 datetime |
| end-time-before | String - ISO8601 datetime |
| end-time-after | String - ISO8601 datetime |
| edited-time-before | String - ISO8601 datetime |
| edited-time-after | String - ISO8601 datetime |
| sort-by | String - start-time | end-time |
| sort-direction | String - asc | desc (default: asc) |
| limit | Integer - 1 to 1000 |
| code | String - filter by run code |
| name | String - filter by run name |
| run-number | Integer - filter by run number (≥ 0) |
| run-status | String - completed | success | failure | abort |
| owner | String - filter by run owner email. A run's owner is the owner of the procedure at the time the run was started |
| procedure-type | Array - procedure | testPlan | testSequence | reference |
| version | String - filter by run version |
| source-run | String - filter by source run id |
| automation-status | String - filter by automation status |
| automation-enabled | Boolean - filter by automation enabled |
| locations | Array - string, filter by step locations. Matches any portion of the location code or name (case-insensitive) |
| linked-runs | Array - string, filter by linked run ids |
| edited-user-id | String - filter by the user id who last edited the run |
| completed-user-id | String - filter by the user id who completed the run |
| auto-procedure-id-enabled | Boolean - filter by auto procedure id setting |
| diagnostic-run-id | String - Returns runs started from a procedure whose release was validated by that diagnostic run. The diagnostic run itself is not returned |
| detail_{field_id} | Filter runs by the run-level custom field with the given field_id. See Custom field filters. |
| procedure_detail_{field_id} | Filter runs by the snapshotted procedure-level custom field with the given field_id. See Custom field filters. |
Query parameter example usage
GET https://api.epsilon3.io/v1/runs/summary?run-state[]=paused&run-state[]=completed&project-ids[]=abc&start-time-before=2024-09-18T01:01:01.000Z
GET https://api.epsilon3.io/v1/runs/summary?detail_priority=High&detail_mass:gte=10&procedure_detail_external_id=ABC-123
Custom field filters on runs summaries
The summary endpoints accept dynamic query parameters that filter results by the value of a custom field. The parameter name encodes both the field and an optional operator.
detail_{field_id}=valuefilters by the entity's own custom field (run-level on/v1/runs/summary, procedure-level on/v1/procedures/summary).procedure_detail_{field_id}=valueis only available on/v1/runs/summaryand filters by the snapshotted procedure-level field as recorded on the run.- An operator suffix may be appended after a colon:
detail_{field_id}:{op}=value. When the suffix is omitted, the operator defaults toeq. - Repeating the same key creates an OR-set of accepted operands for that operator (e.g.,
detail_priority=High&detail_priority=Mediummatches runs whosepriorityisHighorMedium). - The empty string
""is a sentinel that matches "no value recorded".
Supported operators per custom field type:
| Field type | Allowed operators |
|---|---|
Number, Date, Time, Date Time |
eq, ne, gt, gte, lt, lte |
Text |
eq, ne, contains |
List, Custom List, Checkbox |
eq, ne |
External Data |
None — External Data fields cannot be filtered |
Operand format per type: ISO-8601 strings for Date / Time / Date Time, numeric strings for Number, plain strings for Text / List / Custom List, true or false for Checkbox.
contains matches Text values that include the case insensitive operand anywhere, does not accept empty strings or wildcards.
For List fields, an operand matches either the selected option's display text or its option_id (see Recorded Custom Field Values). Filtering by option_id keeps matching after an option is renamed. Values recorded without an option_id can only be matched by their text.
A List or Custom List value with multiple selections matches eq when any selection equals the operand, and matches ne only when no selection does. For a multi select List, each selection can match by its display text or its option_id.
Example Response Body
{
"_id": string,
"code": string,
"completedAt": string, timestamp in ISO-8601 format,
"name": string,
"operation": string,
"project_id": string,
"run_tags": [
{"id": string, "name": string},
...
],
"run_section": (optional) string,
"source_run": (optional) string,
"starttime": string, timestamp in ISO-8601 format,
"state": "running" | "completed" | "paused",
"status": "success" | "failure" | "abort",
"skipped_steps": integer,
"completed_steps": integer,
"failed_steps": integer,
"total_steps": integer,
"not_required_steps": integer,
"participant_user_ids": array(string),
"parent_reference": (optional) {"type": string, "id": string | number},
"details": (optional) Array<CustomFieldValue>,
"procedure_details": (optional) Array<CustomFieldValue>
}
details carries the run-level custom field values, and procedure_details carries a snapshot of the procedure-level custom field values as they were when the run was started. Both arrays include an entry for every custom field added to the procedure's header, even when no value has been recorded — see Recorded Custom Field Values for the wire shape.
parent_reference is present when the run was started from another entity, such as a schedule event, an issue, a work order, or a part. See RunParentReference for its types and ID formats. A run started from another run sets source_run instead.
End a running procedure
This endpoint allows you to end a procedure with an optional comment or status. When a running procedure is ended via this endpoint, any telemetry data that has not been recorded will be stored.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/end',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"comment": "Everything looks OK.",
"status": "success"
}
)
curl -X POST \
https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/end \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"comment": "Everything looks OK.",
"status": "success"
}'
$apiUrl = "https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/end"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"comment": "Everything looks OK.",
"status": "success"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"comment": string (optional),
"status": string (optional)
}
Allowed status values are:
- success
- failure
- abort
POST https://api.epsilon3.io/v1/runs/:runId/end
The runId parameter in the URL must contain the id of the running procedure to end.
Bulk end running procedures
This endpoint ends several running procedures in a single request, with a status and an optional comment applied to all of them. The runs are ended the same way a single run is ended but asynchronously in the background, and the user who made the request is notified when the batch finishes.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/runs/end',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"run_ids": ["up9goX1AgzCuffxUXbpI1K", "lkC6ZrPrvPnPU4OAqNYcW4"],
"comment": "Everything looks OK.",
"status": "success"
}
)
curl -X POST \
https://api.epsilon3.io/v1/runs/end \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"run_ids": ["up9goX1AgzCuffxUXbpI1K", "lkC6ZrPrvPnPU4OAqNYcW4"],
"comment": "Everything looks OK.",
"status": "success"
}'
$apiUrl = "https://api.epsilon3.io/v1/runs/end"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"run_ids": ["up9goX1AgzCuffxUXbpI1K", "lkC6ZrPrvPnPU4OAqNYcW4"],
"comment": "Everything looks OK.",
"status": "success"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"run_ids": Array<string> (required),
"comment": string (optional),
"status": string (optional)
}
run_ids must contain between 1 and 500 run ids.
Allowed status values are:
- success
- failure
- abort
POST https://api.epsilon3.io/v1/runs/end
Blocked Response
{
"blocked": [
{
"run_id": string,
"code": string,
"reason": string
}
]
}
Retrieve available run tags
This endpoint allows you to retrieve all run tags available to be assigned to a run.
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/runs/tags',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
)
curl -X GET \
"https://api.epsilon3.io/v1/runs/tags" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/runs/tags"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/runs/tags
Example Response Body
{
"run_tags": [
{
"id": "qji7zlB0WV",
"name": "Sample Tag"
}
]
}
Create run tags
This endpoint allows you to create tags that you can then assign to runs.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/runs/tags',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={ "tag_names": ["Sample Tag 1", "Sample Tag 2"] }
)
curl -X POST \
https://api.epsilon3.io/v1/runs/tags \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{ "tag_names": ["Sample Tag 1", "Sample Tag 2"] }'
$apiUrl = "https://api.epsilon3.io/v1/runs/tags"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{ "tag_names": ["Sample Tag 1", "Sample Tag 2"] } | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{ "tag_names": ["Sample Tag 1", "Sample Tag 2"] }
POST https://api.epsilon3.io/v1/runs/tags
Example Response Body
{
"data": [
{
"id": "8NqRk2vXc4LmTz7WbYp1Hd",
"name": "Sample Tag 1"
},
{
"id": "3FwGj9sKa6PeUh0ZnDx5Vr",
"name": "Sample Tag 2"
}
]
}
Retrieve run metadata
This endpoint allows you to retrieve the metadata of a run.
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/metadata',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
)
curl -X GET \
"https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/metadata" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/metadata"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/runs/:runId/metadata
The runId parameter in the URL must contain the id of the running procedure to update.
Example Response Body
{
"state": "running",
"run_tags": [
{
"id": "qji7zlB0WV",
"name": "Sample Tag"
}
],
"procedure_tags": [
{
"id": "Wd2pX8mQ4n",
"name": "Sample Procedure Tag"
}
],
"project": {
"code": "P1",
"id": "OFJsa3MWU7",
"name": "ProjectOne"
},
"operation": {
"name": "Sample Operation",
"id": "sample operation"
},
"participants": ["test1@epsilon3.io", "test2@epsilon3.io"],
"details": (optional) Array<CustomFieldValue>,
"procedure_details": (optional) Array<CustomFieldValue>
}
details carries the run-level custom field values, and procedure_details carries a snapshot of the procedure-level custom field values as they were when the run was started. Both arrays include an entry for every custom field added to the procedure's header, even when no value has been recorded — see Recorded Custom Field Values for the wire shape.
Retrieve run steps
This endpoint allows you to retrieve a run's steps as paginated pages, with optional filters for section and step ids.
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/steps',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
params={
"limit": 100,
}
)
curl -X GET \
"https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/steps?limit=100" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/steps?limit=100"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/runs/:runId/steps
The runId parameter in the URL must contain the id of the running procedure whose steps you want to retrieve.
This endpoint accepts these endpoint-specific filters:
| Query parameter | Value |
|---|---|
| section-id | Array - string, filter to steps in the given section ids |
| step-id | Array - string, filter to the given step ids |
Query parameter example usage
GET https://api.epsilon3.io/v1/runs/:runId/steps?step-id[]=sampleStepId1&step-id[]=sampleStepId2
This endpoint is paginated. In addition to any endpoint-specific filters, it accepts these pagination query parameters:
{
"data": [ ... ],
"pagination": {
"limit": integer,
"sort_by": string | null,
"sort_order": "asc" | "desc" | null,
"group_by": string | null,
"current_page_token": string | null,
"previous_page_token": string | null,
"next_page_token": string | null
}
}
| Query parameter | Value |
|---|---|
| limit | Integer - 1 to 1000 (default: 1000) |
| sort-by | String - field to sort the returned items by |
| sort-order | String - asc | desc |
| group-by | String - field to group the returned items by |
| page-token | String - omit for the initial request, or provide the token from a prior response's next_page_token or previous_page_token |
The current page of results is returned in a data array alongside a pagination object that carries the page tokens and metadata used to fetch adjacent pages.
To page forward or backward, send a response's next_page_token or previous_page_token back as the page-token query parameter.
data holds the current page and pagination carries the page tokens and metadata used to fetch adjacent pages. To page forward or backward, send a response's next_page_token or previous_page_token back as the page-token query parameter.
Example Run Step
{
"id": string,
"section_id": string,
"index": integer,
"state": string,
...run step fields (see the `RunStep` object in [Get Run](#get-run))
}
Each item in the data array is a run step:
Record a value on a run step
This endpoint allows you to record a value on a content block of a running procedure, or on one of its procedure variables. A content block is targeted by its section, step and content ids, which are returned by GET /v1/runs/:runId/steps, and the request declares the block's input_type. Attachment inputs are currently supported, on steps and on procedure variables: see Recording a procedure variable.
The request can be sent as JSON, recording attachments that were already uploaded, or as multipart/form-data with one or more files to upload the files and record them in the same request.
Sample Request (JSON)
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/recorded-values',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"input_type": "attachment",
"section_id": "section_Bz2aD4fGh1",
"step_id": "step_Gi2nyYKZjf",
"content_id": "content_Qw8eRt5yUi",
"action_id": "torque-photo-1",
"recorded": {
"value": [
{
"attachment_id": "f48c1df3-dc39-4a1d-90aa-30205f9e8653"
}
]
}
}
)
curl -X POST \
https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/recorded-values \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"input_type": "attachment",
"section_id": "section_Bz2aD4fGh1",
"step_id": "step_Gi2nyYKZjf",
"content_id": "content_Qw8eRt5yUi",
"action_id": "torque-photo-1",
"recorded": {
"value": [
{
"attachment_id": "f48c1df3-dc39-4a1d-90aa-30205f9e8653"
}
]
}
}'
$apiUrl = "https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/recorded-values"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"input_type" = "attachment"
"section_id" = "section_Bz2aD4fGh1"
"step_id" = "step_Gi2nyYKZjf"
"content_id" = "content_Qw8eRt5yUi"
"action_id" = "torque-photo-1"
"recorded" = @{
"value" = @(
@{
"attachment_id" = "f48c1df3-dc39-4a1d-90aa-30205f9e8653"
}
)
}
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Sample Request (file upload)
import requests
import os
with open('torque.png', 'rb') as file:
requests.post(
url='https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/recorded-values',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
data={
'input_type': 'attachment',
'section_id': 'section_Bz2aD4fGh1',
'step_id': 'step_Gi2nyYKZjf',
'content_id': 'content_Qw8eRt5yUi',
'action_id': 'torque-photo-1',
},
files={'file': file}
)
curl -X POST \
https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/recorded-values \
-u "$EPSILON3_API_KEY": \
-F "input_type=attachment" \
-F "section_id=section_Bz2aD4fGh1" \
-F "step_id=step_Gi2nyYKZjf" \
-F "content_id=content_Qw8eRt5yUi" \
-F "action_id=torque-photo-1" \
-F "file=@torque.png"
$apiUrl = "https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/recorded-values"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$form = @{
"input_type" = "attachment"
"section_id" = "section_Bz2aD4fGh1"
"step_id" = "step_Gi2nyYKZjf"
"content_id" = "content_Qw8eRt5yUi"
"action_id" = "torque-photo-1"
"file" = Get-Item -Path "torque.png"
}
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers @{
"Authorization" = "Basic $authHeader"
} -Form $form
Request Body (JSON)
{
"input_type": "attachment",
"section_id": string,
"step_id": string,
"content_id": string,
"timestamp": string (optional),
"action_id": string (optional),
"recorded": {
"value": [
{
"attachment_id": string
}
]
}
}
POST https://api.epsilon3.io/v1/runs/:runId/recorded-values
The runId parameter in the URL must contain the id of the running procedure. This endpoint requires a User API key, since the value is recorded as the key's user and that user must be able to operate the step.
| Field | Value |
|---|---|
| input_type | String - the type of the targeted input or procedure variable. Must be attachment |
| section_id | String (required for a step) - the id of the section containing the step. Leave out to record a procedure variable |
| step_id | String (required for a step) - the id of the step containing the content block. Leave out to record a procedure variable |
| content_id | String - the id of the attachment input to record on, or of the procedure variable |
| timestamp | String (optional) - when the value was recorded, as an ISO 8601 date-time. Defaults to the time of the request |
| action_id | String (optional) - an id for this recording. Retrying a request with an action_id that was already recorded on the block records nothing again, and uploads nothing again, so a request can be safely retried |
| recorded.value | Array - the attachments to record on the block, up to 100. The array replaces what is currently recorded, so include any existing attachments you want to keep. An empty array clears the block. Optional when uploading files |
| attachment_id | String (UUID) - the id of an uploaded file, such as one returned by the files API. Optional for the entries describing uploaded files |
| name | String (optional) - the name to store an uploaded file under, up to 255 characters. Only accepted when uploading files, and defaults to the name the file was uploaded with, which must then also be 255 characters or less. Non-ASCII file names are read as UTF-8 |
The name, content type and size recorded for each attachment are read from its stored file, so they always describe the file that was uploaded.
Uploading files
To upload files and record them in one request, send the request as multipart/form-data with the fields above as form fields and each file in a file field. Repeat the file field to upload up to 10 files in one request. Each file must not exceed 500 MB. The content type of each file is taken from the Content-Type of its form part, which most HTTP clients set from the file extension.
Without recorded, each uploaded file is recorded as a new attachment, under the name it was uploaded with, replacing what is currently recorded. To keep existing attachments, or to choose the name or id of an uploaded file, send recorded.value as indexed form fields, such as recorded[value][0][attachment_id].
When sent, recorded.value holds two kinds of entries:
- An existing attachment reference has the
attachment_idof a file already stored in the workspace, such as one returned by the files API or already recorded on the block. It keeps that attachment recorded and is not paired with a file part. - A new-file entry describes one of the files uploaded by this request. Its
attachment_idis omitted, in which case an id is generated, or is an id that no stored file has, or whose upload never finished, in which case the file is stored under that id.
recorded.value must contain exactly one new-file entry for each file part, and may contain any number of existing attachment references. New-file entries are paired with the file parts in order, skipping the references: the first new-file entry describes the first file, and so on.
The files are stored only once the request passes every other check. Either every file is recorded or none is: if any file cannot be stored, or the value cannot be recorded, the files already stored by the request are removed.
Sample Request (several files, keeping an existing attachment)
curl -X POST \
https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/recorded-values \
-u "$EPSILON3_API_KEY": \
-F "input_type=attachment" \
-F "section_id=section_Bz2aD4fGh1" \
-F "step_id=step_Gi2nyYKZjf" \
-F "content_id=content_Qw8eRt5yUi" \
-F "recorded[value][0][attachment_id]=f48c1df3-dc39-4a1d-90aa-30205f9e8653" \
-F "recorded[value][1][name]=torque-after.png" \
-F "recorded[value][2][name]=gauge.jpg" \
-F "file=@torque.png" \
-F "file=@gauge.jpg"
Recording a procedure variable
To record attachments on one of the run's attachment procedure variables, leave out section_id and step_id, and send the id of the variable as content_id. Variable ids are returned in the variables of GET /v1/runs/:runId, and input_type must match the input type of the variable, attachment. recorded.value takes the same attachments as for a step, and files can be uploaded the same way, as multipart/form-data. A procedure variable can only be recorded once: once it has a value, further requests for it return 409. So a variable is never left without attachments, an empty recorded.value returns 422 for a procedure variable instead of clearing it.
Sample Request (procedure variable)
curl -X POST \
https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/recorded-values \
-u "$EPSILON3_API_KEY": \
-F "input_type=attachment" \
-F "content_id=variable_Hq7cKx2LbR" \
-F "file=@torque.png"
Set run metadata
This endpoint allows you to set metadata about the run. Currently only run tags supported.
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/metadata',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={ "run_tag_ids": ["qji7zlB0WV"] }
)
curl -X PATCH \
https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/metadata \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{ "run_tag_ids": ["qji7zlB0WV"] }'
$apiUrl = "https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/metadata"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{ "run_tag_ids": ["qji7zlB0WV"] } | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
Request Body
{ "run_tag_ids": ["qji7zlB0WV"] }
PATCH https://api.epsilon3.io/v1/runs/:runId/metadata
The runId parameter in the URL must contain the id of the running procedure to update.
Start automation
This endpoint allows you to start automation for a run.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/automation/start',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={ "step_id": "step_Gi2nyYKZjf", "section_id": "sctn_zk5SU0lzbF" }
)
curl -X POST \
https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/automation/start \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{ "step_id": "step_Gi2nyYKZjf", "section_id": "sctn_zk5SU0lzbF" }'
$apiUrl = "https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/automation/start"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{ "step_id": "step_Gi2nyYKZjf", "section_id": "sctn_zk5SU0lzbF" } | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
POST https://api.epsilon3.io/v1/runs/:runId/automation/start
Parameters
| Parameter | Type | Optional | Description |
|---|---|---|---|
| step_id | string | True | Step ID, if provided, automation will only run that step. If not provided, automation will run from the first automatable step. |
| section_id | string | True | Section ID, if provided, automation will only run steps in that section. If not provided, automation will run from the first automatable step. |
The runId parameter in the URL must contain the id of the running procedure to update.
Stop automation
This endpoint allows you to stop automation for a run.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/automation/stop',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={ "step_id": "step_Gi2nyYKZjf", "section_id": "sctn_zk5SU0lzbF" }
)
curl -X POST \
https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/automation/stop \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{ "step_id": "step_Gi2nyYKZjf", "section_id": "sctn_zk5SU0lzbF" }'
$apiUrl = "https://api.epsilon3.io/v1/runs/up9goX1AgzCuffxUXbpI1K/automation/stop"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{ "step_id": "step_Gi2nyYKZjf", "section_id": "sctn_zk5SU0lzbF" } | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
POST https://api.epsilon3.io/v1/runs/:runId/automation/stop
The runId parameter in the URL must contain the id of the running procedure to update.
Search API
Full-text search across procedures, runs, issues, parts, work orders, and shift logs.
Example 403 Response (Team API key)
{
"error": "Team API keys are not supported for this search endpoint. Please use a User API key instead (User -> Profile -> API Keys)."
}
Search
Searches procedures, runs, issues, parts, work orders, and shift logs in the authenticated user's team. Restrict the search to a single entity type with the resource_type query parameter, or omit it (or pass all) to search across every type at once. Results are post-filtered by the user's view permissions, so only items the user can see are returned. Results are ranked by relevance and return the first 100 matches by default.
The query is interpreted with Postgres web-search syntax, with a prefix wildcard on the last term:
- Plain terms are AND'd together —
valve checkmatches results containing bothvalveandcheck. - Quoted phrases match exactly —
"valve check". - Use
orbetween terms for disjunction —valve or pump. - Prefix
-excludes a term —valve -leak. - The last term auto-prefixes —
valmatchesvalve.
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/search?resource_type=all&from_date=2025-01-01&to_date=2025-12-31',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"query": "valve check",
"filters": {
"procedures": ["released"],
"runs": ["running", "paused"]
},
"include_archived": False
}
)
curl -X POST \
'https://api.epsilon3.io/v1/search?resource_type=all&from_date=2025-01-01&to_date=2025-12-31' \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"query": "valve check",
"filters": {
"procedures": ["released"],
"runs": ["running", "paused"]
},
"include_archived": false
}'
$apiUrl = "https://api.epsilon3.io/v1/search?resource_type=all&from_date=2025-01-01&to_date=2025-12-31"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"query" = "valve check"
"filters" = @{
"procedures" = @("released")
"runs" = @("running", "paused")
}
"include_archived" = $false
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Restrict the search to one entity type with resource_type, and narrow each type with its own filter. The example below shows a request for every entity type.
Sample Request (procedures, runs, issues, parts, work orders, shift logs)
import requests
import os
api_key = os.environ.get('EPSILON3_API_KEY')
# Procedures: filter by state, optionally include archived
requests.post(
url='https://api.epsilon3.io/v1/search?resource_type=procedures',
auth=(api_key, ''),
json={"query": "valve check", "filters": {"procedures": ["released", "in_review"]}, "include_archived": False}
)
# Runs: filter by state and by participant (me and/or user ids)
requests.post(
url='https://api.epsilon3.io/v1/search?resource_type=runs&participant=me',
auth=(api_key, ''),
json={"query": "valve check", "filters": {"runs": ["running", "paused"]}}
)
# Issues: filter by system status (draft, active, resolved, closed)
requests.post(
url='https://api.epsilon3.io/v1/search?resource_type=issues',
auth=(api_key, ''),
json={"query": "seal leak", "filters": {"issues": ["active"]}}
)
# Parts: filter by release state
requests.post(
url='https://api.epsilon3.io/v1/search?resource_type=parts',
auth=(api_key, ''),
json={"query": "regulator", "filters": {"parts": ["released"]}}
)
# Work orders: filter by status
requests.post(
url='https://api.epsilon3.io/v1/search?resource_type=work_orders',
auth=(api_key, ''),
json={"query": "assembly", "filters": {"work_orders": ["Running", "Planned"]}}
)
# Shift logs: no state filter
requests.post(
url='https://api.epsilon3.io/v1/search?resource_type=shift_logs',
auth=(api_key, ''),
json={"query": "handoff"}
)
# Procedures: filter by state, optionally include archived
curl -X POST 'https://api.epsilon3.io/v1/search?resource_type=procedures' \
-H 'Content-Type: application/json' -u "$EPSILON3_API_KEY": \
-d '{ "query": "valve check", "filters": { "procedures": ["released", "in_review"] }, "include_archived": false }'
# Runs: filter by state and by participant (me and/or user ids)
curl -X POST 'https://api.epsilon3.io/v1/search?resource_type=runs&participant=me' \
-H 'Content-Type: application/json' -u "$EPSILON3_API_KEY": \
-d '{ "query": "valve check", "filters": { "runs": ["running", "paused"] } }'
# Issues: filter by system status (draft, active, resolved, closed)
curl -X POST 'https://api.epsilon3.io/v1/search?resource_type=issues' \
-H 'Content-Type: application/json' -u "$EPSILON3_API_KEY": \
-d '{ "query": "seal leak", "filters": { "issues": ["active"] } }'
# Parts: filter by release state
curl -X POST 'https://api.epsilon3.io/v1/search?resource_type=parts' \
-H 'Content-Type: application/json' -u "$EPSILON3_API_KEY": \
-d '{ "query": "regulator", "filters": { "parts": ["released"] } }'
# Work orders: filter by status
curl -X POST 'https://api.epsilon3.io/v1/search?resource_type=work_orders' \
-H 'Content-Type: application/json' -u "$EPSILON3_API_KEY": \
-d '{ "query": "assembly", "filters": { "work_orders": ["Running", "Planned"] } }'
# Shift logs: no state filter
curl -X POST 'https://api.epsilon3.io/v1/search?resource_type=shift_logs' \
-H 'Content-Type: application/json' -u "$EPSILON3_API_KEY": \
-d '{ "query": "handoff" }'
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
# Procedures: filter by state, optionally include archived
$body = @{ query = "valve check"; filters = @{ procedures = @("released", "in_review") }; include_archived = $false } | ConvertTo-Json
Invoke-RestMethod -Uri "https://api.epsilon3.io/v1/search?resource_type=procedures" -Method Post -Headers $headers -Body $body
# Runs: filter by state and by participant (me and/or user ids)
$body = @{ query = "valve check"; filters = @{ runs = @("running", "paused") } } | ConvertTo-Json
Invoke-RestMethod -Uri "https://api.epsilon3.io/v1/search?resource_type=runs&participant=me" -Method Post -Headers $headers -Body $body
# Issues: filter by system status (draft, active, resolved, closed)
$body = @{ query = "seal leak"; filters = @{ issues = @("active") } } | ConvertTo-Json
Invoke-RestMethod -Uri "https://api.epsilon3.io/v1/search?resource_type=issues" -Method Post -Headers $headers -Body $body
# Parts: filter by release state
$body = @{ query = "regulator"; filters = @{ parts = @("released") } } | ConvertTo-Json
Invoke-RestMethod -Uri "https://api.epsilon3.io/v1/search?resource_type=parts" -Method Post -Headers $headers -Body $body
# Work orders: filter by status
$body = @{ query = "assembly"; filters = @{ work_orders = @("Running", "Planned") } } | ConvertTo-Json
Invoke-RestMethod -Uri "https://api.epsilon3.io/v1/search?resource_type=work_orders" -Method Post -Headers $headers -Body $body
# Shift logs: no state filter
$body = @{ query = "handoff" } | ConvertTo-Json
Invoke-RestMethod -Uri "https://api.epsilon3.io/v1/search?resource_type=shift_logs" -Method Post -Headers $headers -Body $body
Request Body
{
"query": string,
"filters": (optional) {
"procedures": (optional) [string],
"runs": (optional) [string],
"issues": (optional) [string],
"parts": (optional) [string],
"work_orders": (optional) [string]
},
"include_archived": (optional) boolean
}
POST https://api.epsilon3.io/v1/search
Query parameters:
| Parameter | Type | Description |
|---|---|---|
resource_type |
(optional) string | Restrict to one resource type. One of all, procedures, runs, issues, parts, work_orders, shift_logs. Defaults to all. |
from_date |
(optional) string | ISO 8601 date or datetime. Return resources with updated_at on or after this value. A date-only value (YYYY-MM-DD) is inclusive of the whole day. |
to_date |
(optional) string | ISO 8601 date or datetime. Return resources with updated_at on or before this value. A date-only value (YYYY-MM-DD) is inclusive of the whole day. |
participant |
(optional) string | Comma-separated list of me and/or user ids. Restricts run results to runs that any listed user participated in. Has no effect on other resource types. |
limit |
(optional) integer | Page size, between 1 and 1000. Pass limit to receive a paged response with pagination metadata. |
page-token |
(optional) string | Opaque cursor from a previous response's pagination.next_page_token. Pass it to fetch the next page. |
Body fields:
| Field | Type | Description |
|---|---|---|
query |
string | Required. Between 1 and 255 characters. See the query syntax notes above. |
filters.procedures |
(optional) [string] | Restrict procedure results to these states. Each value 1–100 chars. Typical values: draft, in_review, released. |
filters.runs |
(optional) [string] | Restrict run results to these states. Each value 1–100 chars. Typical values: running, paused, completed. |
filters.issues |
(optional) [string] | Restrict issue results to these statuses. Each value 1–100 chars. Filter by the system status, not the team's display label: draft, active, resolved, closed. |
filters.parts |
(optional) [string] | Restrict part results to these release states. Each value 1–100 chars. Typical values: draft, in_review, released. |
filters.work_orders |
(optional) [string] | Restrict work order results to these statuses. Each value 1–100 chars. Typical values: Planned, Running, Complete, Canceled. |
include_archived |
(optional) boolean | Defaults to false. When false, archived procedures are excluded. Has no effect on other resource types. |
Example 400 Response (incompatible filter)
{
"error": "filters.runs cannot be used when resource_type is procedures"
}
If multiple filter resource types are incompatible, they are joined into the error message (e.g. filters.runs, filters.procedures cannot be used when resource_type is ...).
Example Response Body
{
"data": [
{
"id": "up9goX1AgzCuffxUXbpI1K",
"type": "procedure",
"title": "Valve Leak Check",
"headline": "... pressurize the line and check the <mark>valve</mark> for leaks ...",
"project_id": "h78svHgfi4",
"metadata": {
"code": "PROC-0012",
"name": "Valve Leak Check",
"state": "released",
"editedAt": "2025-09-14T17:22:01.000Z"
}
},
{
"id": "bIkugfdu94hG42jWkfbh2",
"sub_id": "step-3a1f",
"sub_type": "step",
"type": "run",
"title": "Valve Leak Check - Run 42",
"headline": "Open the manual <mark>valve</mark> and record the pressure",
"project_id": "h78svHgfi4",
"metadata": {
"code": "PROC-0012",
"name": "Valve Leak Check",
"run_number": 42,
"state": "running",
"editedAt": "2025-09-15T09:10:33.000Z"
}
}
]
}
Example Results (issues, parts, work orders, shift logs)
{
"data": [
{
"id": "418",
"type": "issue",
"title": "Valve seal leak during pressurization",
"headline": "operator noted a <mark>valve</mark> seal leak while pressurizing the line",
"project_id": "h78svHgfi4",
"metadata": {
"title": "Valve seal leak during pressurization",
"state": "Open",
"severity": "Major",
"reference_id": "ISS-0007"
}
},
{
"id": "Kd0fj2Lm8pQr4sTv6wXy1",
"type": "part",
"title": "PN-1002 - Pressure Regulator",
"headline": "<mark>Pressure</mark> Regulator, 3000 psi inlet",
"project_id": null,
"metadata": {
"name": "Pressure Regulator",
"part_no": "PN-1002",
"revision": "B",
"release_state": "released"
}
},
{
"id": "Wo42gHn2pZ8kLm5qRt3vY",
"type": "work_order",
"title": "WO-42 Regulator Assembly",
"headline": "assemble the <mark>regulator</mark> per drawing REG-1002",
"project_id": "h78svHgfi4",
"metadata": {
"name": "Regulator Assembly",
"work_order_number": 42,
"state": "Running",
"priority": "High"
}
},
{
"id": "sL2Bd9xkQ0",
"type": "shift_log",
"title": "Shift Log 2025-09-14",
"headline": "handoff notes: <mark>valve</mark> bench check complete, regulator staged",
"project_id": null,
"metadata": {
"date": "2025-09-14",
"user_id": "usr_7fJ2kd"
}
}
]
}
Each result includes:
| Field | Type | Description |
|---|---|---|
id |
string | Resource id. |
type |
string | One of procedure, run, issue, part, work_order, shift_log. |
title |
string | Display title for the result. |
sub_id |
(optional) string | Id of the matched sub-resource (e.g. a section or step within a procedure or run). Procedure and run results only. |
sub_type |
(optional) string | section, step, or BOM. Present when the match is on a sub-resource of a procedure or run. |
headline |
(optional) string | Snippet of matching content. Matched terms are wrapped in <mark>...</mark> HTML tags. |
project_id |
(optional) string \ | null |
metadata |
(optional) object | Type-specific summary of the result. Its fields depend on type, as described below. |
The metadata object varies by result type:
type |
metadata fields |
|---|---|
procedure |
code, name, state (draft, in_review, released), editedAt (ISO 8601). |
run |
code, name, run_number, state (running, paused, completed), editedAt (ISO 8601). |
issue |
title, state (the team's display label, e.g. Open), severity (e.g. Major), reference_id (optional). |
part |
name, part_no, revision, release_state (draft, in_review, released). |
work_order |
name, work_order_number (integer), state (Planned, Running, Complete, Canceled), priority (optional). |
shift_log |
date (YYYY-MM-DD), user_id. |
Results are ordered by descending relevance. Use pagination when you need more than the first 100 matches.
Pagination
Search returns the first 100 relevance-ranked matches by default so quick lookups stay small and predictable. If you are syncing search results, exporting matches, or building a UI that needs more than the first page, pass limit and follow the returned cursor until it is exhausted.
- Set
limit(1–1000) to choose the page size and receive pagination metadata. - Read
pagination.next_page_tokenfrom the response and pass it back aspage-tokento fetch the next page. - Treat
page-tokenas opaque. Do not parse or build it yourself. - When
next_page_tokenisnull, there are no more results.
Sample Paged Request (next page)
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/search?resource_type=all&limit=100&page-token=eyJvZmZzZXQiOjEwMH0',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={"query": "valve check"}
)
curl -X POST \
'https://api.epsilon3.io/v1/search?resource_type=all&limit=100&page-token=eyJvZmZzZXQiOjEwMH0' \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{ "query": "valve check" }'
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{ query = "valve check" } | ConvertTo-Json
Invoke-RestMethod -Uri "https://api.epsilon3.io/v1/search?resource_type=all&limit=100&page-token=eyJvZmZzZXQiOjEwMH0" -Method Post -Headers $headers -Body $body
Example Paged Response Body
{
"data": [ "... search results ..." ],
"pagination": {
"next_page_token": "eyJvZmZzZXQiOjEwMH0",
"limit": 100
}
}
| Field | Type | Description |
|---|---|---|
pagination.next_page_token |
string \ | null |
pagination.limit |
integer | The page size applied to this response. |
Settings API
Locations
Manage locations that can be assigned to procedure steps.
List Locations
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/settings/locations',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/settings/locations \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/settings/locations"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/settings/locations
Retrieve all locations available in your workspace.
Example Response Body
{
"locations": [
{
"id": 1,
"uid": "loc_abc123",
"code": "BAY-1",
"name": "Bay 1",
"parent_location_id": null
},
{
"id": 2,
"uid": "loc_def456",
"code": "BAY-1-A",
"name": "Bay 1 Section A",
"parent_location_id": 1
}
]
}
Response
The response status code will be 200 and return { "locations": [...] }.
Each location object contains:
| Field | Type | Description |
|---|---|---|
id |
integer | Unique identifier for the location |
uid |
string | Unique string identifier |
code |
string | Short code for the location |
name |
string | Display name of the location |
parent_location_id |
integer or null | ID of the parent location, if any |
usage_declarations |
array | Usage types the location declares, in the same shape as List Usage Types. Present only when the location declares any. |
usage_limits |
array | The location's usage limits, each with usage_type_id, enforcement, and whichever of limit_min, limit_max and limit_warn_max are set. Present only when the location has any. |
Create Locations
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/settings/locations',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"locations": [
{ "code": "BAY-1", "name": "Bay 1" },
{ "code": "BAY-1-A", "name": "Bay 1 Section A", "parent_location_id": 1 }
]
}
)
curl -X POST \
https://api.epsilon3.io/v1/settings/locations \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"locations": [
{ "code": "BAY-1", "name": "Bay 1" },
{ "code": "BAY-1-A", "name": "Bay 1 Section A", "parent_location_id": 1 }
]
}'
$apiUrl = "https://api.epsilon3.io/v1/settings/locations"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
locations = @(
@{ code = "BAY-1"; name = "Bay 1" },
@{ code = "BAY-1-A"; name = "Bay 1 Section A"; parent_location_id = 1 }
)
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
POST https://api.epsilon3.io/v1/settings/locations
Create one or more locations in your workspace.
Example Request Body
{
"locations": [
{ "code": "BAY-1", "name": "Bay 1" },
{ "code": "BAY-1-A", "name": "Bay 1 Section A", "parent_location_id": 1 }
]
}
Request Body
locations: an array (1–1000) of objects, each with the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | yes | Short code, 1–255 characters. Must be unique across all locations. |
name |
string | yes | Display name, 1–255 characters |
parent_location_id |
integer | no | ID of an existing location to use as the parent |
parent_code |
string | no | Code of another location in the same request to use as the parent (useful for bulk creation with hierarchies) |
Example Response Body
{
"added_locations": [
{
"id": 1,
"uid": "loc_abc123",
"code": "BAY-1",
"name": "Bay 1",
"parent_location_id": null
},
{
"id": 2,
"uid": "loc_def456",
"code": "BAY-1-A",
"name": "Bay 1 Section A",
"parent_location_id": 1
}
]
}
Response
The response status code will be 201 and return { "added_locations": [...] } containing the created location objects.
Update Location
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v1/settings/locations/2',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={ "name": "Bay 1 Section B" }
)
curl -X PATCH \
https://api.epsilon3.io/v1/settings/locations/2 \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{ "name": "Bay 1 Section B" }'
$apiUrl = "https://api.epsilon3.io/v1/settings/locations/2"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
name = "Bay 1 Section B"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
PATCH https://api.epsilon3.io/v1/settings/locations/:id
Update one or more fields on a specific location. The request body must contain at least one field.
Example Request Body
{
"name": "Bay 1 Section B",
"code": "BAY-1-B",
"parent_location_id": null
}
Example Request Body (Usage)
{
"usage_declaration_ids": [7, 10],
"usage_limits": [
{ "usage_type_id": 7, "limit_min": 15, "limit_max": 30, "enforcement": "block" }
]
}
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | no | Short code, 1–255 characters. Must be unique across all locations. |
name |
string | no | Display name, 1–255 characters |
parent_location_id |
integer or null | no | ID of the parent location. Pass null to remove the parent. |
usage_declaration_ids |
array of integers | no | 0–50 IDs of usage types the location declares, so usage can be recorded against it. See Usage. Replaces the location's declarations: pass [] to remove them all. Removing a declaration also removes its usage limit. Archived usage types cannot be newly declared. |
usage_limits |
array of objects | no | 0–50 life limits on declared usage types, each with usage_type_id, limit_min, limit_max, limit_warn_max and enforcement, following the same rules as on Create Tools. Replaces the location's limits: a declared usage type left out loses its limit, and [] removes them all. A limit can name a usage type declared in the same request. |
Response
The response status code will be 204 with no body.
Delete Locations
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/settings/locations',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={ "location_ids": [1, 2] }
)
curl -X DELETE \
https://api.epsilon3.io/v1/settings/locations \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{ "location_ids": [1, 2] }'
$apiUrl = "https://api.epsilon3.io/v1/settings/locations"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
location_ids = @(1, 2)
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers $headers -Body $body
DELETE https://api.epsilon3.io/v1/settings/locations
Delete one or more locations by ID.
Example Request Body
{
"location_ids": [1, 2]
}
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
location_ids |
array of integers | yes | IDs of the locations to delete (1–1000 items) |
Response
The response status code will be 204 with no body.
Units
Manage the units of measure available to number inputs and other fields in your workspace.
Each unit is defined by three values, and all three must be unique within your workspace:
| Field | Type | Description |
|---|---|---|
id |
integer | Unique identifier for the unit |
name |
string | Singular name of the unit, for example gram |
plural |
string | Plural name of the unit, for example grams |
abbreviation |
string | Short form of the unit, for example g |
List Units
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/settings/units',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/settings/units \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/settings/units"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/settings/units
Retrieve all units defined in your workspace, ordered by name. This endpoint takes no query parameters.
Example Response Body
{
"units": [
{
"id": 1,
"name": "gram",
"plural": "grams",
"abbreviation": "g"
},
{
"id": 2,
"name": "meter",
"plural": "meters",
"abbreviation": "m"
}
]
}
Response
The response status code will be 200 and return { "units": [...] }.
Create Unit
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/settings/units',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"name": "gram",
"plural": "grams",
"abbreviation": "g"
}
)
curl -X POST \
https://api.epsilon3.io/v1/settings/units \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"name": "gram",
"plural": "grams",
"abbreviation": "g"
}'
$apiUrl = "https://api.epsilon3.io/v1/settings/units"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
name = "gram"
plural = "grams"
abbreviation = "g"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
POST https://api.epsilon3.io/v1/settings/units
Create a single unit. This endpoint creates one unit per request and is not idempotent — sending the same body twice returns a duplicate error rather than the existing unit.
Example Request Body
{
"name": "gram",
"plural": "grams",
"abbreviation": "g"
}
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Singular name. Must be unique across all units. |
plural |
string | yes | Plural name. Must be unique across all units. |
abbreviation |
string | yes | Short form. Must be unique across all units. |
Example Response Body
{
"id": 1
}
Response
The response status code will be 201 and return the new unit's id.
Update Unit
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v1/settings/units/1',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={ "abbreviation": "gm" }
)
curl -X PATCH \
https://api.epsilon3.io/v1/settings/units/1 \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{ "abbreviation": "gm" }'
$apiUrl = "https://api.epsilon3.io/v1/settings/units/1"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
abbreviation = "gm"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
PATCH https://api.epsilon3.io/v1/settings/units/:id
Update one or more fields on a specific unit. Fields that are left out are unchanged.
Example Request Body
{
"name": "gram",
"plural": "grams",
"abbreviation": "gm"
}
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | Singular name. Must be unique across all units. |
plural |
string | no | Plural name. Must be unique across all units. |
abbreviation |
string | no | Short form. Must be unique across all units. |
Response
The response status code will be 204 with no body.
Delete Unit
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/settings/units/1',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X DELETE \
https://api.epsilon3.io/v1/settings/units/1 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/settings/units/1"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers $headers
DELETE https://api.epsilon3.io/v1/settings/units/:id
Delete a single unit by ID.
Response
The response status code will be 204 with no body.
Duplicate Unit Errors
Because name, plural, and abbreviation must each be unique within your workspace, POST and PATCH return HTTP 400 when a value is already taken:
| Message | Cause |
|---|---|
Duplicate unit name |
Another unit already uses that name |
Duplicate unit plural |
Another unit already uses that plural |
Duplicate unit abbreviation |
Another unit already uses that abbreviation |
Pause Reasons
Retrieve the pause reasons configured for your workspace, so you can send a valid pause_reason_id when pausing a run.
List Pause Reasons
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/settings/pause-reasons',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/settings/pause-reasons \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/settings/pause-reasons"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/settings/pause-reasons
Retrieve the pause reason configuration for your workspace.
Example Response Body
{
"mode": "required",
"reasons": [
{
"id": "aB3dEf7hIjKlMnOpQrStUv",
"text": "Weather hold"
}
]
}
Response
The response status code will be 200.
| Field | Type | Description |
|---|---|---|
mode |
string | off, optional, or required |
reasons |
array | The configured reasons, each with an id to send as pause_reason_id and the text shown to operators |
mode tells you whether a pause must name a reason. When it is required, a pause request without a pause_reason_id returns 400, and the error lists the valid ids. When it is optional, naming a reason is allowed but not required. When it is off, no pause reason is recorded, and sending a pause_reason_id returns 400 — omit the field. A workspace that has never configured pause reasons reports off with an empty reasons array.
Skills API
Supports managing skills and user skill assignments. Skills represent certifications or qualifications that users can hold, optionally with expiration and renewal tracking.
List Skills
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/skills',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/skills \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/skills"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/skills
Retrieve all skills defined in your workspace. All query parameters below are optional; when several are supplied they are combined with AND, and omitting them all returns every skill. Text filters (code, name, description) match case-insensitively on any substring, while the remaining filters match exactly.
| Query parameter | Value |
|---|---|
| code | String - case-insensitive partial match on skill code |
| name | String - case-insensitive partial match on skill name |
| description | String - case-insensitive partial match on description |
| renewal-interval-unit | String - day | month | year |
| renewal-interval-count | Integer - exact match (≥ 1) |
| renewal-method | String - external | procedure |
| procedure-id | String - exact match on the linked procedure id |
| archived | Boolean - filter by archived state |
| created-at-before | String - ISO8601 datetime, skills created at or before this time |
| created-at-after | String - ISO8601 datetime, skills created at or after this time |
| updated-at-before | String - ISO8601 datetime, skills updated at or before this time |
| updated-at-after | String - ISO8601 datetime, skills updated at or after this time |
Query parameter example usage
GET https://api.epsilon3.io/v1/skills?archived=false&renewal-method=procedure&name=egress&created-at-after=2024-01-01T00:00:00.000Z
Unknown parameters, or values that fail validation (for example an invalid renewal_method or a malformed date), return HTTP 422.
Example Response Body
{
"data": [
{
"id": "2001",
"team_id": "team_abc123",
"code": "PILOT-IPC",
"name": "IFR Instrument Proficiency Check",
"description": "If pilot instrument currency lapses for 12 months or greater, IPC is required",
"renewal_interval": "yearly",
"renewal_interval_count": 1,
"renewal_interval_unit": "year",
"renewal_method": "external",
"procedure_id": null,
"archived": false,
"created_at": "2024-01-15T10:00:00.000Z",
"updated_at": "2024-06-01T12:00:00.000Z"
},
{
"id": "2002",
"team_id": "team_abc123",
"code": "EGRESS-RECURRENT",
"name": "Egress System Recurrent Training",
"description": "Recurrent egress training required every two years",
"renewal_interval": null,
"renewal_interval_count": 2,
"renewal_interval_unit": "year",
"renewal_method": "external",
"procedure_id": null,
"archived": false,
"created_at": "2024-02-01T10:00:00.000Z",
"updated_at": "2024-06-01T12:00:00.000Z"
}
]
}
The second skill above uses a custom cadence (every two years), so its deprecated renewal_interval is null — only renewal_interval_count and renewal_interval_unit describe it.
Get Skill
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/skills/2002',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/skills/2002 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/skills/2002"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/skills/:skillId
Retrieve a single skill with all of its associated data.
Example Response Body
{
"skill": {
"id": "2002",
"team_id": "team_abc123",
"code": "EGRESS-RECURRENT",
"name": "Egress System Recurrent Training",
"description": "Recurrent egress training required every two years",
"renewal_interval": null,
"renewal_interval_count": 2,
"renewal_interval_unit": "year",
"renewal_method": "external",
"procedure_id": null,
"archived": false,
"created_at": "2024-02-01T10:00:00.000Z",
"updated_at": "2024-06-01T12:00:00.000Z"
},
"users": [
{
"id": "3001",
"team_id": "team_abc123",
"skill_id": "2002",
"user_id": "user_xyz789",
"received_date": "2024-01-20T00:00:00.000Z",
"expires_date": "2026-01-20T00:00:00.000Z",
"last_renewed_date": null,
"active_renewal_run_id": null,
"created_at": "2024-01-20T10:00:00.000Z",
"updated_at": "2024-01-20T10:00:00.000Z"
}
],
"linked_roles": [
{
"id": "1003",
"name": "Crew",
"code": "CREW"
}
]
}
Create Skill
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/skills',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"code": "PILOT-IPC",
"name": "IFR Instrument Proficiency Check",
"description": "If pilot instrument currency lapses for 12 months or greater, IPC is required",
"renewal_interval_count": 1,
"renewal_interval_unit": "year",
"renewal_method": "external"
}
)
curl -X POST \
https://api.epsilon3.io/v1/skills \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"code": "PILOT-IPC",
"name": "IFR Instrument Proficiency Check",
"description": "If pilot instrument currency lapses for 12 months or greater, IPC is required",
"renewal_interval_count": 1,
"renewal_interval_unit": "year",
"renewal_method": "external"
}'
$apiUrl = "https://api.epsilon3.io/v1/skills"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"code" = "PILOT-IPC"
"name" = "IFR Instrument Proficiency Check"
"description" = "If pilot instrument currency lapses for 12 months or greater, IPC is required"
"renewal_interval_count" = 1
"renewal_interval_unit" = "year"
"renewal_method" = "external"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"code": string,
"name": string,
"description": string (optional),
"renewal_interval_count": integer >= 1 (optional),
"renewal_interval_unit": "day" | "month" | "year" (optional),
"renewal_interval": "yearly" | "monthly" | "daily" (optional, deprecated),
"renewal_method": "external" | "procedure",
"procedure_id": string (optional)
}
POST https://api.epsilon3.io/v1/skills
Create a new skill. The code field must be unique within your workspace and cannot exceed 50 characters. The name field cannot exceed 255 characters.
Use renewal_interval_count and renewal_interval_unit together to set how often the skill expires — for example 2 + "year" for a bi-yearly skill, or 5 + "day". The two fields must be supplied together; providing only one returns HTTP 400. Omit both for a skill that never expires.
Set renewal_method to "procedure" and provide a procedure_id to link a procedure that users must complete to renew the skill. Set renewal_method to "external" for skills renewed outside of Epsilon3.
Example Response Body
{
"data": {
"id": "2001",
"team_id": "team_abc123",
"code": "PILOT-IPC",
"name": "IFR Instrument Proficiency Check",
"description": "If pilot instrument currency lapses for 12 months or greater, IPC is required",
"renewal_interval": "yearly",
"renewal_interval_count": 1,
"renewal_interval_unit": "year",
"renewal_method": "external",
"procedure_id": null,
"archived": false,
"created_at": "2024-01-15T10:00:00.000Z",
"updated_at": "2024-01-15T10:00:00.000Z"
}
}
Update Skill
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v1/skills/2001',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"name": "IFR Instrument Proficiency Check (Updated)",
"archived": false
}
)
curl -X PATCH \
https://api.epsilon3.io/v1/skills/2001 \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"name": "IFR Instrument Proficiency Check (Updated)",
"archived": false
}'
$apiUrl = "https://api.epsilon3.io/v1/skills/2001"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"name" = "IFR Instrument Proficiency Check (Updated)"
"archived" = $false
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
Request Body
{
"name": string (optional),
"description": string | null (optional),
"renewal_interval_count": integer >= 1 | null (optional),
"renewal_interval_unit": "day" | "month" | "year" | null (optional),
"renewal_interval": "yearly" | "monthly" | "daily" | null (optional, deprecated),
"renewal_method": "external" | "procedure" (optional),
"procedure_id": string | null (optional),
"archived": boolean (optional)
}
PATCH https://api.epsilon3.io/v1/skills/:skillId
Update an existing skill. All fields are optional — only the fields provided will be updated. Pass null for description or procedure_id to clear those values.
To change the renewal cadence, send renewal_interval_count and renewal_interval_unit together; send both as null to make the skill never expire. Supplying only one of the pair returns HTTP 400.
Example Response Body
{
"data": {
"id": "2001",
"team_id": "team_abc123",
"code": "PILOT-IPC",
"name": "IFR Instrument Proficiency Check (Updated)",
"description": "If pilot instrument currency lapses for 12 months or greater, IPC is required",
"renewal_interval": "yearly",
"renewal_interval_count": 1,
"renewal_interval_unit": "year",
"renewal_method": "external",
"procedure_id": null,
"archived": false,
"created_at": "2024-01-15T10:00:00.000Z",
"updated_at": "2024-06-10T09:00:00.000Z"
}
}
Assign Users to Skill
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/skills/2001/assign',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"user_ids": ["user_xyz789", "user_abc123"]
}
)
curl -X POST \
https://api.epsilon3.io/v1/skills/2001/assign \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"user_ids": ["user_xyz789", "user_abc123"]
}'
$apiUrl = "https://api.epsilon3.io/v1/skills/2001/assign"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"user_ids" = @("user_xyz789", "user_abc123")
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"user_ids": [string]
}
POST https://api.epsilon3.io/v1/skills/:skillId/assign
Assign one or more users to a skill. If a user is already assigned, they are silently skipped. A maximum of 100 user IDs may be provided per request.
Example Response Body
{
"data": [
{
"id": "3001",
"team_id": "team_abc123",
"skill_id": "2001",
"user_id": "user_xyz789",
"received_date": "2024-06-10T00:00:00.000Z",
"expires_date": "2025-06-10T00:00:00.000Z",
"last_renewed_date": null,
"active_renewal_run_id": null,
"created_at": "2024-06-10T09:00:00.000Z",
"updated_at": "2024-06-10T09:00:00.000Z"
}
]
}
Unassign Users from Skill
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/skills/2001/unassign',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"user_ids": ["user_xyz789"]
}
)
curl -X POST \
https://api.epsilon3.io/v1/skills/2001/unassign \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"user_ids": ["user_xyz789"]
}'
$apiUrl = "https://api.epsilon3.io/v1/skills/2001/unassign"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"user_ids" = @("user_xyz789")
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"user_ids": [string]
}
POST https://api.epsilon3.io/v1/skills/:skillId/unassign
Remove one or more users from a skill. Users who do not have the skill assigned are silently skipped. A maximum of 100 user IDs may be provided per request.
Renew User Skill
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/skills/2001/renew',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"user_id": "user_xyz789",
"last_renewed_date": "2024-06-10"
}
)
curl -X POST \
https://api.epsilon3.io/v1/skills/2001/renew \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"user_id": "user_xyz789",
"last_renewed_date": "2024-06-10"
}'
$apiUrl = "https://api.epsilon3.io/v1/skills/2001/renew"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"user_id" = "user_xyz789"
"last_renewed_date" = "2024-06-10"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"user_id": string,
"last_renewed_date": string (optional, ISO-8601 date)
}
POST https://api.epsilon3.io/v1/skills/:skillId/renew
Record a renewal for a user's skill. Updates the last_renewed_date and recalculates expires_date based on the skill's renewal cadence (renewal_interval_count + renewal_interval_unit). If last_renewed_date is omitted, the current date is used.
Example Response Body
{
"data": {
"id": "3001",
"team_id": "team_abc123",
"skill_id": "2001",
"user_id": "user_xyz789",
"received_date": "2024-01-20T00:00:00.000Z",
"expires_date": "2025-06-10T00:00:00.000Z",
"last_renewed_date": "2024-06-10T00:00:00.000Z",
"active_renewal_run_id": null,
"created_at": "2024-01-20T10:00:00.000Z",
"updated_at": "2024-06-10T09:00:00.000Z"
}
}
Start Skill Renewal Run
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/skills/2001/start-renewal-run',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"user_id": "user_xyz789"
}
)
curl -X POST \
https://api.epsilon3.io/v1/skills/2001/start-renewal-run \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"user_id": "user_xyz789"
}'
$apiUrl = "https://api.epsilon3.io/v1/skills/2001/start-renewal-run"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"user_id" = "user_xyz789"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"user_id": string
}
POST https://api.epsilon3.io/v1/skills/:skillId/start-renewal-run
Start a procedure run to renew a user's skill. The skill must have renewal_method set to "procedure" with a linked procedure_id. Only one active renewal run per user per skill is allowed at a time.
Example Response Body
{
"run_id": "GMFvqA1qLgrw0xIEc95M2o",
"run_number": 42
}
The run can be viewed at https://app.epsilon3.io/runs/<run_id>.
Snippets API
Supports listing, retrieving, creating, updating, archiving, and deleting reusable snippets (steps, sections, and content blocks) that can be embedded into procedures.
List Snippets
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/snippets',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
params={
'snippet-type': 'step',
'created-at-after': '2024-01-01T00:00:00Z'
}
)
curl -X GET \
'https://api.epsilon3.io/v1/snippets?snippet-type=step&created-at-after=2024-01-01T00:00:00Z' \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/snippets?snippet-type=step&created-at-after=2024-01-01T00:00:00Z"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/snippets
Retrieve all snippets in your workspace. All query parameters are optional and can be combined to narrow results.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
include_deleted |
boolean | When true, includes soft-deleted snippets in results. Defaults to false. |
include_archived |
boolean | When true, includes archived snippets in results. Defaults to false. |
snippet_ids |
string[] | Filter by snippet IDs. Accepts up to 50 IDs per request. |
test_snippet |
boolean | When true, returns test snippets instead of regular snippets. Defaults to false. |
name |
string | Case-insensitive partial match on snippet name. |
description |
string | Case-insensitive partial match on snippet description. |
snippet-type |
string | Filter by snippet type. One of step, section, or content_block. |
created-by |
string | Filter by the email of the user who created the snippet. |
edited-by |
string | Filter by the email of the user who last edited the snippet. |
created-at-before |
ISO 8601 date | Return snippets created before this date. |
created-at-after |
ISO 8601 date | Return snippets created after this date. |
edited-at-before |
ISO 8601 date | Return snippets last edited before this date. |
edited-at-after |
ISO 8601 date | Return snippets last edited after this date. |
Example Response Body
{
"data": [
{
"_id": "snp_abc123",
"_rev": "3-snp_abc123",
"snippet_id": "snp_abc123",
"name": "Safety Check",
"type": "snippet",
"snippet_type": "step",
"deleted": false,
"archived_at": null,
"description": "Standard safety verification step",
"created_at": "2024-06-01T10:00:00.000Z",
"created_by": "user@epsilon3.io",
"edited_at": "2024-09-15T14:30:00.000Z",
"edited_by": "user@epsilon3.io",
"revision": "3",
"procedures": [],
"step": {
"id": "step_xyz",
"name": "Safety Check",
"signoffs": [{ "id": "signoff_1", "operators": [] }],
"content": []
}
},
{
"_id": "snp_def456",
"_rev": "1-snp_def456",
"snippet_id": "snp_def456",
"name": "Pre-Flight Section",
"type": "snippet",
"snippet_type": "section",
"deleted": false,
"archived_at": null,
"created_at": "2024-07-10T08:00:00.000Z",
"created_by": "admin@epsilon3.io",
"edited_at": "2024-07-10T08:00:00.000Z",
"edited_by": "admin@epsilon3.io",
"revision": "1",
"procedures": [],
"section": {
"id": "section_abc",
"name": "Pre-Flight Section",
"steps": []
}
}
]
}
The response shape depends on snippet_type: step snippets include a step field, section snippets include a section field, and content block snippets include a content_block field.
Get Snippet
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/snippets/snp_abc123',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/snippets/snp_abc123 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/snippets/snp_abc123"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/snippets/{snippetId}
Retrieve a single snippet by id, including the procedures it is used in.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
test_snippet |
boolean | When true, looks up a snippet in the Testing Module instead of a regular snippet. Defaults to false. |
include_deleted |
boolean | When true, a soft-deleted snippet can still be retrieved. Defaults to false. |
Example Response Body
{
"_id": "snp_abc123",
"_rev": "3-snp_abc123",
"snippet_id": "snp_abc123",
"name": "Safety Check",
"type": "snippet",
"snippet_type": "step",
"deleted": false,
"archived_at": null,
"description": "Standard safety verification step",
"created_at": "2024-06-01T10:00:00.000Z",
"created_by": "user@epsilon3.io",
"edited_at": "2024-09-15T14:30:00.000Z",
"edited_by": "user@epsilon3.io",
"revision": "3",
"procedures": [
{
"procedure_id": "procedure_123",
"element_id": "step_xyz",
"location": "step",
"state": "released",
"code": "PRO-042",
"name": "Vehicle Checkout"
}
],
"step": {
"id": "step_xyz",
"name": "Safety Check",
"signoffs": [{ "id": "signoff_1", "operators": [] }],
"content": []
}
}
The procedures array lists each place the snippet is embedded, with the procedure and the element location within it.
Archived snippets are always returned by this endpoint; archived_at holds the time the snippet was archived, or null if it is not archived.
Create Snippet
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/snippets',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"type": "snippet",
"snippet_type": "step",
"name": "Safety Check",
"description": "Standard safety verification step",
"data": {
"id": "step_xyz",
"name": "Safety Check",
"signoffs": [{ "id": "signoff_1", "operators": [] }],
"content": []
}
}
)
curl -X POST \
https://api.epsilon3.io/v1/snippets \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"type": "snippet",
"snippet_type": "step",
"name": "Safety Check",
"description": "Standard safety verification step",
"data": {
"id": "step_xyz",
"name": "Safety Check",
"signoffs": [{ "id": "signoff_1", "operators": [] }],
"content": []
}
}'
$apiUrl = "https://api.epsilon3.io/v1/snippets"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"type" = "snippet"
"snippet_type" = "step"
"name" = "Safety Check"
"description" = "Standard safety verification step"
"data" = @{
"id" = "step_xyz"
"name" = "Safety Check"
"signoffs" = @(@{ "id" = "signoff_1"; "operators" = @() })
"content" = @()
}
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"type": "snippet" | "test_snippet",
"snippet_type": "step" | "section" | "content_block",
"name": string,
"description": string (optional),
"data": object
}
POST https://api.epsilon3.io/v1/snippets
Create a new snippet. The server generates the snippet id and returns it in the response. The name field cannot exceed 255 characters.
The data object holds the snippet contents and must match the snippet_type: a step object for step, a section object for section, or a content block object for content_block. The shapes match the step, section, and content_block fields returned by the list and get endpoints.
The data object is validated against its snippet_type; requests that do not match return HTTP 422 naming the offending field:
| Snippet type | Required data shape |
|---|---|
step |
id (non-empty string), name (string), content (array of blocks, each with a non-empty type), signoffs (array of objects, each with a non-empty id and an operators array), and optionally location (string, at most 512 characters), expected_duration (string in HH:MM:SS format), and timer (object whose time_left is an ISO 8601 duration, for example PT1H30M). |
section |
id (non-empty string), name (string), and steps (array of step objects, each validated with the step rules above; step ids must be unique within the section). |
content_block |
An object with a non-empty type. |
Content block ids and signoff ids, where present, must be unique across the whole snippet — including across the steps of a section snippet.
Additional fields beyond these are accepted and stored as sent.
The type field selects the snippet kind: snippet creates a regular snippet and test_snippet creates a test snippet.
Example Response Body
{
"id": "snp_HGJSjGHZ5AgaoDmy48tvRl",
"rev": "1",
"ok": true
}
Update Snippet
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v1/snippets/snp_abc123',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"type": "snippet",
"snippet_type": "step",
"name": "Safety Check",
"description": "Standard safety verification step",
"data": {
"id": "step_xyz",
"name": "Safety Check",
"signoffs": [{ "id": "signoff_1", "operators": [] }],
"content": []
}
}
)
curl -X PATCH \
https://api.epsilon3.io/v1/snippets/snp_abc123 \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"type": "snippet",
"snippet_type": "step",
"name": "Safety Check",
"description": "Standard safety verification step",
"data": {
"id": "step_xyz",
"name": "Safety Check",
"signoffs": [{ "id": "signoff_1", "operators": [] }],
"content": []
}
}'
$apiUrl = "https://api.epsilon3.io/v1/snippets/snp_abc123"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"type" = "snippet"
"snippet_type" = "step"
"name" = "Safety Check"
"description" = "Standard safety verification step"
"data" = @{
"id" = "step_xyz"
"name" = "Safety Check"
"signoffs" = @(@{ "id" = "signoff_1"; "operators" = @() })
"content" = @()
}
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
Request Body
{
"type": "snippet" | "test_snippet",
"snippet_type": "step" | "section" | "content_block",
"name": string,
"description": string (optional),
"data": object
}
PATCH https://api.epsilon3.io/v1/snippets/{snippetId}
Update an existing snippet. The request body fully replaces the snippet's content — send the complete snippet, not just the changed fields. Each successful update increments the snippet's revision.
The data object is validated the same way as on Create Snippet; requests that do not match the shape for their snippet_type return HTTP 422.
The type and snippet_type fields are immutable: they must match the values the snippet was created with.
Archived snippets cannot be updated. Requests to update an archived snippet return HTTP 400; unarchive the snippet first.
Example Response Body
{
"id": "snp_abc123",
"rev": "4",
"ok": true
}
Delete Snippet
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/snippets/snp_abc123',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X DELETE \
https://api.epsilon3.io/v1/snippets/snp_abc123 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/snippets/snp_abc123"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers $headers
DELETE https://api.epsilon3.io/v1/snippets/{snippetId}
Soft-delete a snippet. Deleted snippets no longer appear in list or get responses unless include_deleted=true is passed.
Example Response Body
{
"id": "snp_abc123",
"rev": "5",
"ok": true
}
Archive Snippet
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/snippets/snp_abc123/archive',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X POST \
https://api.epsilon3.io/v1/snippets/snp_abc123/archive \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/snippets/snp_abc123/archive"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers
POST https://api.epsilon3.io/v1/snippets/{snippetId}/archive
Archive a snippet. Archived snippets can no longer be added to procedures or updated, and no longer appear in list responses unless include_archived=true is passed. Procedures that already use the snippet are not changed. Archiving does not change the snippet's revision, and archiving an already archived snippet keeps its original archived_at.
Example Response Body
{
"id": "snp_abc123",
"rev": "4",
"ok": true
}
Unarchive Snippet
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/snippets/snp_abc123/unarchive',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X POST \
https://api.epsilon3.io/v1/snippets/snp_abc123/unarchive \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/snippets/snp_abc123/unarchive"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers
POST https://api.epsilon3.io/v1/snippets/{snippetId}/unarchive
Unarchive a snippet so it can be added to procedures and updated again. Unarchiving does not change the snippet's revision.
Example Response Body
{
"id": "snp_abc123",
"rev": "4",
"ok": true
}
Swimlanes API
Swimlanes are used in schedules to group events in the Gantt chart. Epsilon3's swimlanes API currently supports the following capabilities.
Get Swimlanes
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/planning/swimlanes',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/planning/swimlanes \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/planning/swimlanes"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET /api/v1/planning/swimlanes
Retrieves all swimlane
Response Body
{
"swimlanes": [
{
"id": "DpSTO81RP9fZSUd8d8ffY4",
"name": "Swimlane Exo"
},
{
"id": "9ozH0ZOXLLBRZg4K0v1CXf",
"name": "New"
},
{
"id": "A2T07M4MkQeiJQqW7xOH6w",
"name": "API Swimlane"
},
{
"id": "F1HKrIQ8gY6UIHRj1qhclC",
"name": "Launch"
},
{
"id": "zAt5hyS3FEmvy4THcTIIVh",
"name": "QA Check"
}
]
}
Update Swimlane
PATCH /api/v1/planning/swimlanes/:swimlane_id
Updates a specific swimlane.
Request URL
PATCH /api/v1/planning/swimlanes/test_swimlane_id
Example Request Body
{
"name": "Patched Swimlane"
}
Parameters
swimlane_id(path parameter): Id of the swimlane.
Request Body
The request body should be a JSON object that contains the details of the swimlane to be updated. The following fields are supported:
name: (required) Swimlane name, must be a string.
Response The response status code will be 204 for a succcessful message
Create Swimlane
POST /api/v1/planning/swimlanes
Request URL
POST /api/v1/planning/swimlanes
Example Request Body
{
"name": "New Swimlane"
}
Creates a new swimlane.
Parameters
No path parameters are required for this endpoint.
Request Body
The request body should be a JSON object that contains the details of the swimlane to be created. The following fields are supported:
name: (required) Swimlane name, must be a string.
Response
The response status code will be 201 and return a JSON object containing the ID of the newly created swimlane.
Delete Swimlane
DELETE /api/v1/planning/swimlanes/:swimlane_id
Request URL
DELETE /api/v1/planning/swimlanes/test_swimlane_id
Deletes an swimlane.
Parameters
swimlane_id(path parameter): Id of the swimlane.
Response
The response status code will be 204 for a successful response.
Telemetry API
Epsilon3’s telemetry API supports creating, updating, retrieving, or deleting telemetry parameters. Uploading telemetry parameters to the API allows the user to do the following in Epsilon3:
- Search by parameter name for manual input of telemetry values in procedures
- Display parameter description and automatically populate units
Create or update a telemetry parameter
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/telemetry/parameters',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"name": "test.simulation.error",
"type": "enum",
"dictionary_id": 1,
"description": "Payload error flag",
"refresh_rate_ms": 10000,
"values": { "0": "FAIL", "1": "SUCCESS" }
}
)
curl -X POST \
https://api.epsilon3.io/v1/telemetry/parameters \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"name": "test.simulation.error",
"type": "enum",
"dictionary_id": 1,
"description": "Payload error flag",
"refresh_rate_ms": 10000,
"values": { "0": "FAIL", "1": "SUCCESS" }
}'
$apiUrl = "https://api.epsilon3.io/v1/telemetry/parameters"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"name": "test.simulation.error",
"type": "enum",
"dictionary_id": 1,
"description": "Payload error flag",
"refresh_rate_ms": 10000,
"values": { "0": "FAIL", "1": "SUCCESS" }
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body (Parameter Object)
{
"name": string (1-255 characters),
"type": string (float, int, string, bool, enum, or aggregate),
"dictionary_id": integer (optional, minimum 1),
"description": string (optional, 1-255 characters),
"units": string (optional, 1-255 characters),
"refresh_rate_ms": integer (optional, 1000-2147483647),
"values": object (optional - required for enum and aggregate types)
}
POST https://api.epsilon3.io/v1/telemetry/parameters
The request body should contain a Parameter object. The fields name and type are required. The name field should be unique per dictionary. The optional dictionary_id field allows the parameter to be related to a dictionary. If no dictionary_id is provided, the parameter will be related to the default dictionary. The parameter refresh_rate_ms is an integer denoting the number of milliseconds the UI will wait before refreshing the value, defaults to 1000 (1 second) if not provided. The minimum value is 1000 (1 second) and the maximum is 2,147,483,647. The optional units field must be between 1 and 255 characters if provided. The values parameter is required for enum and aggregate type parameters. For enum types, values represents a mapping of integer keys to string labels - each integer key corresponds to a unique string label. For aggregate types, values represents an array of parameter objects - each object has a required name and type field (a values field is required if type is enum)
Response Body
{
"id": string,
"name": string,
"type": string,
"dictionary_id": integer,
"created_at": string, timestamp in ISO-8601 format
}
Example Enum Request
{
"name": "test.simulation.enum",
"type": "enum",
"dictionary_id": 5,
"description": "Enum representing state of simulation",
"refresh_rate_ms": 1000,
"values": { "0": "FAIL", "1": "SUCCESS" }
}
Example Aggregate Request
{
"name": "test.simulation.aggregate",
"type": "aggregate",
"dictionary_id": 5,
"description": "Aggregate type representing state of simulation",
"refresh_rate_ms": 1000,
"values": [{"name": "test_val", "type": "float"}, {"name": "test_enum", "type": "enum", "values":{ "0": "FAIL", "1": "SUCCESS" }}]
}
Supported types:
floatintstringboolenumaggregate
Bulk create or update telemetry parameters
Sample Request
import requests
import os
requests.put(
url='https://api.epsilon3.io/v1/telemetry/parameters/bulk',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"parameters": [
{
"name": "test.voltage",
"type": "float",
"units": "V",
"description": "Battery voltage",
"refresh_rate_ms": 2000
},
{
"name": "test.status",
"type": "enum",
"values": { "0": "OFF", "1": "ON" }
}
]
}
)
curl -X PUT \
https://api.epsilon3.io/v1/telemetry/parameters/bulk \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"parameters": [
{
"name": "test.voltage",
"type": "float",
"units": "V",
"description": "Battery voltage",
"refresh_rate_ms": 2000
},
{
"name": "test.status",
"type": "enum",
"values": { "0": "OFF", "1": "ON" }
}
]
}'
$apiUrl = "https://api.epsilon3.io/v1/telemetry/parameters/bulk"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"parameters" = @(
@{
"name" = "test.voltage"
"type" = "float"
"units" = "V"
"description" = "Battery voltage"
"refresh_rate_ms" = 2000
},
@{
"name" = "test.status"
"type" = "enum"
"values" = @{ "0" = "OFF"; "1" = "ON" }
}
)
} | ConvertTo-Json -Depth 4
Invoke-RestMethod -Uri $apiUrl -Method Put -Headers $headers -Body $body
Request Body
{
"parameters": array (1 to 1000 items) [
{
"name": string (1-255 characters),
"type": string (float, int, string, bool, enum, or aggregate),
"dictionary_id": integer (optional, minimum 1),
"description": string (optional, 1-255 characters),
"units": string (optional, 1-255 characters),
"refresh_rate_ms": integer (optional, 1000-2147483647),
"values": object (optional)
}
]
}
PUT https://api.epsilon3.io/v1/telemetry/parameters/bulk
Creates or updates 1 to 1000 telemetry parameters in a single request. Each parameter in the parameters array accepts the same fields as the single create endpoint. If a parameter with the same name and dictionary_id already exists, it will be updated.
Response Body
{
"parameters": [
{
"id": string,
"name": string,
"type": string,
"dictionary_id": integer,
"created_at": string, timestamp in ISO-8601 format
}
]
}
List all available telemetry parameters
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/telemetry/parameters',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/telemetry/parameters \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/telemetry/parameters"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/telemetry/parameters
Sample Request with Query Parameters
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/telemetry/parameters',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
params={
"dictionary-group-ids": "1,5,28"
}
)
curl -X GET \
"https://api.epsilon3.io/v1/telemetry/parameters?dictionary-group-ids=1,5,28" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/telemetry/parameters?dictionary-group-ids=1,5,28"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/telemetry/parameters?dictionary-group-ids={dictionary-group-ids}
The optional URL parameter dictionary-group-ids may be provided to filter telemetry parameters by dictionary groups. The value should be a comma-separated list of dictionary group IDs (max 1000). The response will only contain telemetry parameters that belong to at least one of the specified dictionary groups.
The optional URL parameter type may be provided to only return telemetry parameters of the given types. The value may be a single type or a repeated parameter (type=enum&type=float). Each type must be one of float, int, string, bool, enum or aggregate.
The response contains at most 400,000 parameters.
Response Body (List of Parameter Objects)
[{
"id": string,
"name": string,
"type": string,
"dictionary_id": integer,
"created_at": string, timestamp in ISO-8601 format,
"refresh_rate_ms": number,
"values": object (Represents a mapping of integer keys to string labels)
}]
List telemetry parameters (paginated)
Sample Request
import requests
import os
api_url = "https://api.epsilon3.io/v2/telemetry/parameters"
params = {"limit": 100, "dictionary-group-ids": [1, 5]}
parameters = []
while True:
response = requests.get(
url=api_url,
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
params=params,
).json()
parameters.extend(response["data"])
next_page_token = response["pagination"]["next_page_token"]
if next_page_token is None:
break
params = {"page-token": next_page_token, "dictionary-group-ids": [1, 5]}
curl -X GET \
"https://api.epsilon3.io/v2/telemetry/parameters?limit=100&dictionary-group-ids=1&dictionary-group-ids=5" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v2/telemetry/parameters?limit=100&dictionary-group-ids=1&dictionary-group-ids=5"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v2/telemetry/parameters
Returns the team's telemetry parameters one page at a time, ordered by parameter name and then dictionary ID.
| Query parameter | Value |
|---|---|
| dictionary-group-ids | Integer - optional dictionary group ID to filter by. Repeat the parameter to pass several groups (max 50). Only parameters that belong to at least one of the groups are returned. |
| type | String - optional parameter type to filter by, one of float, int, string, bool, enum or aggregate. Repeat the parameter to pass several types. When combined with dictionary-group-ids, a parameter still belongs to a group based on all of its dictionary copies, regardless of their types. |
| name | String - optional parameter name to filter by. Repeat the parameter to pass several names (max 50). Every dictionary's copy of a matching name is returned. |
| limit | Integer - 1 to 1000 (default: 1000) |
| page-token | String - omit for the first page, or pass a prior response's next_page_token or previous_page_token |
sort-by, sort-order, and group-by are not supported and return 422. Pass the same dictionary-group-ids, type and name with every page token. A token used with different dictionary-group-ids, type, name or limit is rejected with 400. When limit is omitted from a later page, the first page's limit is used. A dictionary-group-ids value that does not exist returns 404.
Response Body
{
"data": [{
"id": integer,
"name": string,
"type": string,
"dictionary_id": integer,
"created_at": string, timestamp in ISO-8601 format,
"description": string | null,
"units": string | null,
"refresh_rate_ms": integer | null,
"values": object | array | null
}],
"pagination": {
"limit": integer,
"sort_by": null,
"sort_order": null,
"group_by": null,
"current_page_token": string | null,
"previous_page_token": string | null,
"next_page_token": string | null
}
}
Retrieve a telemetry parameter by name
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/telemetry/parameters',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
params={
"name": "test.simulation.error",
"dictionary_id": 1
}
)
curl -X GET \
"https://api.epsilon3.io/v1/telemetry/parameters/query?name=test.simulation.error&dictionary_id=1" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/telemetry/parameters/query?name=test.simulation.error&dictionary_id=1"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/telemetry/parameters/query?name={name}&dictionary_id={dictionary_id}
The URL parameter name is required and must be URL encoded. An optional dictionary_id (integer) may be provided to choose a specific dictionary. If dictionary_id is not included, the default dictionary will be used.
Delete parameters
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/telemetry/parameters',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"ids": [1, 2, 3]
}
)
curl -X DELETE \
https://api.epsilon3.io/v1/telemetry/parameters \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"ids": [1, 2, 3]
}'
$apiUrl = "https://api.epsilon3.io/v1/telemetry/parameters"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"ids" = @(1, 2, 3)
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers $headers -Body $body
DELETE https://api.epsilon3.io/v1/telemetry/parameters
Request Body
{
"ids": array (integer, up to 1000 items)
}
Example Request Body
{
"ids": [123, 456]
}
Telemetry Push API
The telemetry push API sends batches of numeric samples to telemetry parameters that already exist in your workspace. A sample is one value of one parameter at one point in time. Pushed samples are stored over time.
A batch holds one list of timestamps and, for each parameter, one value per timestamp. Optional tags record where the samples came from, such as a test stand or a vehicle. A tag is a key and a value, such as stand and A. All the tags on a parameter together are its tag set. Each tag set is stored separately, so several sources can push the same parameter names at the same time without mixing their stored data.
Push setup
Before pushing you need an API key, the parameters you will push, and any tag keys you plan to use.
API key
Generate a user API key and send it as described in Authentication. Pushing samples, creating tag keys, and deleting tag keys require the key's user to have the Edit Data permission. Listing tag keys and tag sets requires no permission.
Push parameters
Every workspace has a default dictionary. A parameter created without a dictionary_id goes into it. Pushed samples only go to parameters in the default dictionary. Create the parameters first with Create or update a telemetry parameter or Bulk create or update telemetry parameters, and leave dictionary_id unset.
Only float, int, bool and enum parameters accept pushed samples. Push bool values as 0 or 1, and enum values as the integer key of the enum.
Tag keys
Every tag key used in a push must exist before the push. Create them with Create tag keys. A push that uses a key the workspace does not have is rejected with 422 and the key's name.
Push a batch
Sample Request
import gzip
import json
import os
import requests
batch = {
"tags": {"stand": "A"},
"timestamps": [1762200000000, 1762200001000, 1762200002000],
"parameters": [
{"name": "battery.voltage", "values": [27.9, 27.8, 27.8]},
{"name": "battery.current", "values": [1.2, None, 1.4]},
{"name": "heater.state", "values": [1, 1, 0], "tags": {"stand": "B"}},
],
}
requests.post(
url='https://api.epsilon3.io/v1/telemetry/push',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
headers={
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
data=gzip.compress(json.dumps(batch).encode("utf-8")),
)
cat > batch.json <<'EOF'
{
"tags": { "stand": "A" },
"timestamps": [1762200000000, 1762200001000, 1762200002000],
"parameters": [
{ "name": "battery.voltage", "values": [27.9, 27.8, 27.8] },
{ "name": "battery.current", "values": [1.2, null, 1.4] },
{ "name": "heater.state", "values": [1, 1, 0], "tags": { "stand": "B" } }
]
}
EOF
gzip -c batch.json | curl -X POST \
https://api.epsilon3.io/v1/telemetry/push \
-H 'Content-Type: application/json' \
-H 'Content-Encoding: gzip' \
-u "$EPSILON3_API_KEY": \
--data-binary @-
$apiUrl = "https://api.epsilon3.io/v1/telemetry/push"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Content-Encoding" = "gzip"
"Authorization" = "Basic $authHeader"
}
$batch = @{
"tags" = @{ "stand" = "A" }
"timestamps" = @(1762200000000, 1762200001000, 1762200002000)
"parameters" = @(
@{ "name" = "battery.voltage"; "values" = @(27.9, 27.8, 27.8) },
@{ "name" = "battery.current"; "values" = @(1.2, $null, 1.4) },
@{ "name" = "heater.state"; "values" = @(1, 1, 0); "tags" = @{ "stand" = "B" } }
)
} | ConvertTo-Json -Depth 4
$jsonBytes = [Text.Encoding]::UTF8.GetBytes($batch)
$stream = New-Object IO.MemoryStream
$gzip = New-Object IO.Compression.GZipStream($stream, [IO.Compression.CompressionMode]::Compress)
$gzip.Write($jsonBytes, 0, $jsonBytes.Length)
$gzip.Close()
$body = $stream.ToArray()
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"tags": object (optional, tag key to tag value, up to 10 tags),
"timestamps": array (integer, 1 to 500,000 items, milliseconds since the Unix epoch in UTC),
"parameters": array (1 to 500,000 items) [
{
"name": string (1-255 characters, a parameter in the default dictionary),
"values": array (number or null, one per timestamp, in the same order),
"tags": object (optional, tag key to tag value, up to 10 tags)
}
]
}
POST https://api.epsilon3.io/v1/telemetry/push
The request body must be gzip compressed and sent with Content-Encoding: gzip and Content-Type: application/json. A body that is not gzip encoded is rejected with 415.
timestamps lists the sample times once. Each parameter's values array has exactly one entry per timestamp, in the same order. Use null where a parameter has no sample at a timestamp. Nulls are skipped and are not an error. Values must be finite numbers, and enum values must be integers.
Tag keys and values are strings. The batch tags apply to every parameter in the batch. A parameter's own tags are added on top, and its tag value wins for any key that is in both. Every key must be one of the workspace's tag keys. Keys are only checked on parameters that are written, so a batch whose parameters are all skipped returns 200 even if its tags use an unknown key.
The first push that uses a new tag set registers it, and it then appears in List tag sets. Tags are optional. A push without tags writes to the empty tag set.
Response Body
{
"accepted": integer (samples queued for storage),
"skipped_parameters": array (string, parameter names that were not written)
}
Limits
| Limit | Value |
|---|---|
Samples per request (timestamps length × parameters length) |
500,000 |
| Request body (JSON, before gzip) | 8 MB |
| Timestamp | 0 to 9,223,372,036,854 milliseconds since the Unix epoch |
| Parameter name | 1 to 255 characters |
| Tags per tag set | 10 |
| Tag key or tag value | 1 to 64 characters, no backslash |
| Tag keys per workspace | 50 |
| Tag sets per workspace | 100 |
| Push requests | 30 per second per API key |
The push rate limit is separate from the site-wide rate limit and is counted per API key within a workspace, per second. The standard RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers are returned on every push response.
Keys named dictionaryName or fileName, and any key starting with _, are reserved for tags the server sets and cannot be created or pushed.
Push errors
422 Response Body
{
"error": [
{
"type": "field",
"location": "body",
"path": string,
"msg": string
}
]
}
503 Response Body
{
"message": "Server is busy, retry later"
}
| Status | Meaning | What to do |
|---|---|---|
403 |
The workspace does not have the Data module, or the key's user lacks the Edit Data permission | Check the user's role, or contact support about the Data module |
413 |
The JSON body is larger than 8 MB before gzip | Split the batch into smaller requests |
415 |
The body is not gzip encoded | Compress the body and send Content-Encoding: gzip |
422 |
The batch failed validation: more than 500,000 samples, a parameter's values length differs from timestamps, a tag key the workspace does not have, a reserved tag key, a new tag set when the workspace already has 100, or a malformed body |
Fix the batch. The msg names the parameter or tag key at fault. Do not retry unchanged |
429 |
More than 30 push requests in one second from this API key | Wait for the number of seconds in Retry-After, then resend the same batch |
503 |
The server is overloaded or restarting and cannot accept the batch right now | Wait for the number of seconds in Retry-After, then resend the same batch |
Batching recipe
Collect samples as they arrive, then push them in one request every second or when the batch gets large. Keep pushes to a few per second per API key to stay well under the rate limit. Resend on 429 and 503, waiting for the Retry-After value when the server provides one.
Epsilon3 provides a sample Python script that does this: Telemetry push batcher. It is provided as-is for illustration; the API as documented here is the contract. It keeps one batch per tag set, so create one Batcher for each tag set. Its PUSH_SAMPLE_COUNT keeps a batch well under both the 500,000 sample limit and the 8 MB body limit, so a batch never has to be split.
List tag keys
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/telemetry/tag-keys',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/telemetry/tag-keys \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/telemetry/tag-keys"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/telemetry/tag-keys
Returns the workspace's tag keys ordered by name. A workspace has at most 50 keys.
Response Body
{
"data": [{
"id": string (uuid),
"name": string,
"description": string,
"created_at": string, timestamp in ISO-8601 format
}],
"pagination": {
"limit": integer,
"sort_by": null,
"sort_order": null,
"group_by": null,
"current_page_token": string | null,
"previous_page_token": string | null,
"next_page_token": string | null
}
}
Create tag keys
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/telemetry/tag-keys',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"keys": [
{"name": "stand", "description": "Test stand the samples came from"},
{"name": "vehicle"}
]
}
)
curl -X POST \
https://api.epsilon3.io/v1/telemetry/tag-keys \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"keys": [
{ "name": "stand", "description": "Test stand the samples came from" },
{ "name": "vehicle" }
]
}'
$apiUrl = "https://api.epsilon3.io/v1/telemetry/tag-keys"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"keys" = @(
@{ "name" = "stand"; "description" = "Test stand the samples came from" },
@{ "name" = "vehicle" }
)
} | ConvertTo-Json -Depth 3
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"keys": array (1 to 50 items) [
{
"name": string (1-64 characters, no backslash),
"description": string (optional, up to 255 characters)
}
]
}
POST https://api.epsilon3.io/v1/telemetry/tag-keys
Creates every key in the request or none of them. Key names are compared case-insensitively. A key must exist before a push can use it as a tag, and a workspace can have at most 50 keys.
Response Body
{
"data": [{
"id": string (uuid),
"name": string,
"description": string,
"created_at": string, timestamp in ISO-8601 format
}]
}
Error Response Body
{
"message": string
}
| Status | Meaning |
|---|---|
201 |
All keys were created |
409 |
A name repeats within the request, or the workspace already has a key with that name |
422 |
A name is reserved (dictionaryName, fileName, or starts with _), or the workspace would have more than 50 keys |
Delete a tag key
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/telemetry/tag-keys/4c1c6d2e-7f0a-4b7e-9d3b-2f5e8a1c9b70',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X DELETE \
https://api.epsilon3.io/v1/telemetry/tag-keys/4c1c6d2e-7f0a-4b7e-9d3b-2f5e8a1c9b70 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/telemetry/tag-keys/4c1c6d2e-7f0a-4b7e-9d3b-2f5e8a1c9b70"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers $headers
DELETE https://api.epsilon3.io/v1/telemetry/tag-keys/:tagKeyId
The tagKeyId path parameter is the key's id from List tag keys. A key that any tag set contains cannot be deleted, because samples already stored with that tag depend on it.
| Status | Meaning |
|---|---|
204 |
The key was deleted |
404 |
No key with that id |
409 |
A tag set contains the key |
List tag sets
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/telemetry/tag-sets',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/telemetry/tag-sets \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/telemetry/tag-sets"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/telemetry/tag-sets
Returns every tag set the workspace has pushed, oldest first. Tag sets are registered automatically by the first push that uses them. There is no endpoint to create or delete a tag set. A workspace has at most 100 tag sets.
Response Body
{
"data": [{
"id": string (uuid),
"tags": object (tag key to tag value),
"created_at": string, timestamp in ISO-8601 format
}],
"pagination": {
"limit": integer,
"sort_by": null,
"sort_order": null,
"group_by": null,
"current_page_token": string | null,
"previous_page_token": string | null,
"next_page_token": string | null
}
}
List tag sets pushed for a parameter
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/telemetry/tag-sets/parameters/battery.voltage',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/telemetry/tag-sets/parameters/battery.voltage \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/telemetry/tag-sets/parameters/battery.voltage"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/telemetry/tag-sets/parameters/:parameterName
Returns every tag set that has been written for one parameter in the default dictionary. The parameterName path parameter must be URL encoded. An empty tags object is included when samples without tags have been pushed for the parameter.
Response Body
{
"data": [{
"tags": object (tag key to tag value)
}],
"pagination": {
"limit": integer,
"sort_by": null,
"sort_order": null,
"group_by": null,
"current_page_token": string | null,
"previous_page_token": string | null,
"next_page_token": string | null
}
}
Test Management API
Conditions
List Conditions
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/testing/conditions',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/testing/conditions \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/testing/conditions"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/testing/conditions
Retrieve all conditions defined in your workspace.
Example Response Body
{
"data": [
{
"id": "089dab71-f13a-43b1-a350-481c873129f1",
"name": "Speed",
"units": [
"MPH",
"KPH"
]
},
...
]
}
Get Condition
GET https://api.epsilon3.io/v1/testing/conditions/:id
Retrieve a single condtion using its ID.
Parameters
id(path parameter): ID of the condtion being retrieved.
Example Request URL
GET https://api.epsilon3.io/v1/testing/conditions/089dab71-f13a-43b1-a350-481c873129f1
Example Response Body
{
"id": "089dab71-f13a-43b1-a350-481c873129f1",
"name": "Speed",
"units": [
"MPH",
"KPH"
]
}
Hazards
List Hazards
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/testing/risks',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/testing/risks \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/testing/risks"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/testing/risks
Retrieve all hazards defined in your workspace.
Example Response Body
{
"data": [
{
"id": "f2161547-615e-4959-a989-91f3627d20d1",
"name": "Altitude Loss",
"cause": "Stall",
"notes": "Work in progress",
"effect": "Inadequate altitude for recovery",
"category": "Mechanical",
"severity": 0,
"probability": 1,
"mitigation": "Experienced Test Pilot: Ensure a highly skilled test pilot with experience in stall recovery is at the controls. Safety Equipment: Ensure the aircraft is equipped with safety features, such as a stall warning system, to aid in stall recovery.",
"updated_at": "2024-01-24T21:00:39.421Z",
},
...
]
}
Get Hazard
GET https://api.epsilon3.io/v1/testing/risks/:id
Retrieve a single hazard using its ID.
Parameters
id(path parameter): ID of the hazard being retrieved.
Example Request URL
GET https://api.epsilon3.io/v1/testing/risks/f2161547-615e-4959-a989-91f3627d20d1
Example Response Body
{
"id": "f2161547-615e-4959-a989-91f3627d20d1",
"name": "Altitude Loss",
"cause": "Stall",
"notes": "Work in progress",
"effect": "Inadequate altitude for recovery",
"category": "Mechanical",
"severity": 0,
"probability": 1,
"mitigation": "Experienced Test Pilot: Ensure a highly skilled test pilot with experience in stall recovery is at the controls. Safety Equipment: Ensure the aircraft is equipped with safety features, such as a stall warning system, to aid in stall recovery.",
"updated_at": "2024-01-24T21:00:39.421Z",
}
List Hazard Versions
GET https://api.epsilon3.io/v1/testing/risks/:id/versions
Retrieve all the versions for a hazard.
Parameters
id(path parameter): ID of the hazard being retrieved.
Example Request URL
GET https://api.epsilon3.io/v1/testing/risks/f2161547-615e-4959-a989-91f3627d20d1/versions
Example Response Body
{
"data": [
{
"id": 123,
"timestamp": "2024-01-24T21:00:39.421Z",
"record":
{
"id": "f2161547-615e-4959-a989-91f3627d20d1",
"name": "Altitude Loss",
"cause": "Stall",
"notes": "Work in progress",
"effect": "Inadequate altitude for recovery",
"category": "Mechanical",
"severity": 0,
"probability": 1,
"mitigation": "Experienced Test Pilot: Ensure a highly skilled test pilot with experience in stall recovery is at the controls. Safety Equipment: Ensure the aircraft is equipped with safety features, such as a stall warning system, to aid in stall recovery.",
"updated_at": "2024-01-24T21:00:39.421Z",
}
},
...
]
}
Get Hazard Version
GET https://api.epsilon3.io/v1/testing/risks/:id/versions/:versionId
Retrieve a single version for a hazard.
Parameters
id(path parameter): ID of the hazard being retrieved.versionId(path parameter): ID of the version being retrieved.
Example Request URL
GET https://api.epsilon3.io/v1/testing/risks/f2161547-615e-4959-a989-91f3627d20d1/versions/123
Example Response Body
{
"id": 123,
"timestamp": "2024-01-24T21:00:39.421Z",
"record":
{
"id": "f2161547-615e-4959-a989-91f3627d20d1",
"name": "Altitude Loss",
"cause": "Stall",
"notes": "Work in progress",
"effect": "Inadequate altitude for recovery",
"category": "Mechanical",
"severity": 0,
"probability": 1,
"mitigation": "Experienced Test Pilot: Ensure a highly skilled test pilot with experience in stall recovery is at the controls. Safety Equipment: Ensure the aircraft is equipped with safety features, such as a stall warning system, to aid in stall recovery.",
"updated_at": "2024-01-24T21:00:39.421Z",
}
}
Test Points
List Test Points
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/testing/cases',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/testing/cases \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/testing/cases"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/testing/cases
Retrieve all test points defined in your workspace.
Example Response Body
{
"data": [
{
"id": "f2161547-615e-4959-a989-91f3627d20d1",
"name": "Cruise",
"test_case_conditions": [
{
"name": "Speed",
"units": "MPH",
"value": "22",
"condition_id": "089dab71-f13a-43b1-a350-481c873129f1"
}
],
},
...
]
}
Get Test Point
GET https://api.epsilon3.io/v1/testing/cases/:id
Retrieve a single test point using its ID.
Parameters
id(path parameter): ID of the test point being retrieved.
Example Request URL
GET https://api.epsilon3.io/v1/testing/cases/f2161547-615e-4959-a989-91f3627d20d1
Example Response Body
{
"id": "f2161547-615e-4959-a989-91f3627d20d1",
"name": "Cruise",
"test_case_conditions": [
{
"name": "Speed",
"units": "MPH",
"value": "22",
"condition_id": "089dab71-f13a-43b1-a350-481c873129f1"
}
],
"risks": [], // list of associated hazards
"requirements": [], // list of associated requirements
"runs": [], // list of associated runs
}
List Test Point Versions
GET https://api.epsilon3.io/v1/testing/cases/:id/versions
Retrieve all the versions for a test point.
Parameters
id(path parameter): ID of the test point being retrieved.
Example Request URL
GET https://api.epsilon3.io/v1/testing/cases/f2161547-615e-4959-a989-91f3627d20d1/versions
Example Response Body
{
"data": [
{
"id": 123,
"timestamp": "2024-01-24T21:00:39.421Z",
"record":
{
"id": "f2161547-615e-4959-a989-91f3627d20d1",
"name": "Cruise",
"test_case_conditions": [
{
"name": "Speed",
"units": "MPH",
"value": "22",
"condition_id": "089dab71-f13a-43b1-a350-481c873129f1"
}
],
}
},
...
]
}
Get Test Point Version
GET https://api.epsilon3.io/v1/testing/cases/:id/versions/:versionId
Retrieve a single version for a test point.
Parameters
id(path parameter): ID of the test point being retrieved.versionId(path parameter): ID of the version being retrieved.
Example Request URL
GET https://api.epsilon3.io/v1/testing/cases/f2161547-615e-4959-a989-91f3627d20d1/versions/123
Example Response Body
{
"id": 123,
"timestamp": "2024-01-24T21:00:39.421Z",
"record":
{
"id": "f2161547-615e-4959-a989-91f3627d20d1",
"name": "Cruise",
"test_case_conditions": [
{
"name": "Speed",
"units": "MPH",
"value": "22",
"condition_id": "089dab71-f13a-43b1-a350-481c873129f1"
}
],
}
}
Requirements
List Requirements
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/testing/requirements',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/testing/requirements \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/testing/requirements"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/testing/requirements
Retrieve all requirements defined in your workspace. The results can be narrowed using the optional query parameters below. When more than one is provided they are combined with AND.
Query Parameters
custom-id(query parameter): optional. Case-insensitive substring match against the requirement custom ID.text(query parameter): optional. Case-insensitive substring match against the requirement text.type(query parameter): optional. Case-insensitive substring match against the requirement type.notes(query parameter): optional. Case-insensitive substring match against the requirement notes.priority-name(query parameter): optional. Case-insensitive substring match against the priority name (e.g.High).priority-id(query parameter): optional integer. Exact match on the priority ID.status-name(query parameter): optional. Case-insensitive substring match against the status name (e.g.Active).status-id(query parameter): optional integer. Exact match on the status ID.version(query parameter): optional. Exact match on the requirement version.project-id(query parameter): optional. Return only requirements scoped to this project.position(query parameter): optional integer. Exact match on the requirement position.
Example Request URL
GET https://api.epsilon3.io/v1/testing/requirements?type=secondary&priority-name=High&project-id=h78svHgfi4
Example Response Body
{
"data": [
{
"id": "6be45049-ed63-4d53-b839-a0a719f94be9",
"custom_id": "A-100",
"position": 1,
"text": "The aircraft should be a commercial passenger airplane designed for carrying passengers and cargo.",
"priority": {
"id": 1,
"name": "High",
"color": "red-200"
},
"type": "secondary",
"notes": "This is not a military aircraft."
},
...
]
}
Create Requirement
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/testing/requirements',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
'custom_id': 'A-100',
'text': 'The aircraft should be a commercial passenger airplane designed for carrying passengers and cargo.',
'type': 'secondary',
'priority_id': 1
}
)
curl -X POST \
https://api.epsilon3.io/v1/testing/requirements \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{"custom_id":"A-100","text":"The aircraft should be a commercial passenger airplane designed for carrying passengers and cargo.","type":"secondary","priority_id":1}'
$apiUrl = "https://api.epsilon3.io/v1/testing/requirements"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
}
$body = @{
custom_id = "A-100"
text = "The aircraft should be a commercial passenger airplane designed for carrying passengers and cargo."
type = "secondary"
priority_id = 1
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
POST https://api.epsilon3.io/v1/testing/requirements
Create a new requirement in your workspace.
Body Parameters
custom_id(string): required. The requirement custom ID.text(string): optional. The requirement text.position(integer): optional. 1-based position of the requirement in the list.priority_id(integer): optional. ID of the priority to assign.status_id(integer): optional. ID of the status to assign.type(string): optional. The requirement type.notes(string): optional. Freeform notes.case_ids(array of strings): optional. IDs of test points to associate.source_links(array): optional. Links for which this requirement is the source.target_links(array): optional. Links that target this requirement.project_id(string): optional. ID of the project to scope the requirement to.version(string): optional. The requirement version.attachments(array): optional. Up to 100 attachment metadata objects.details(array): optional. Custom field detail values, each with anid.
Example Response Body
{
"id": "6be45049-ed63-4d53-b839-a0a719f94be9",
"custom_id": "A-100",
"position": 1,
"text": "The aircraft should be a commercial passenger airplane designed for carrying passengers and cargo.",
"priority": {
"id": 1,
"name": "High",
"color": "red-200"
},
"type": "secondary",
"notes": "This is not a military aircraft."
}
Import Requirements
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v1/testing/requirements/import',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
'requirements': [
{
'custom_id': 'A-100',
'text': 'The aircraft should be a commercial passenger airplane designed for carrying passengers and cargo.',
'type': 'secondary',
'priority_id': 1
},
{
'custom_id': 'A-101',
'text': 'The aircraft should seat at least 100 passengers.',
'type': 'secondary',
'priority_id': 1
}
]
}
)
curl -X POST \
https://api.epsilon3.io/v1/testing/requirements/import \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{"requirements":[{"custom_id":"A-100","text":"The aircraft should be a commercial passenger airplane designed for carrying passengers and cargo.","type":"secondary","priority_id":1},{"custom_id":"A-101","text":"The aircraft should seat at least 100 passengers.","type":"secondary","priority_id":1}]}'
$apiUrl = "https://api.epsilon3.io/v1/testing/requirements/import"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
}
$body = @{
requirements = @(
@{
custom_id = "A-100"
text = "The aircraft should be a commercial passenger airplane designed for carrying passengers and cargo."
type = "secondary"
priority_id = 1
},
@{
custom_id = "A-101"
text = "The aircraft should seat at least 100 passengers."
type = "secondary"
priority_id = 1
}
)
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
POST https://api.epsilon3.io/v1/testing/requirements/import
Bulk create or update requirements in your workspace. Each requirement is matched on its custom_id: an existing requirement with the same custom_id is updated, otherwise a new one is created. Returns a 204 No Content on success.
Body Parameters
requirements(array): required. Between 1 and 10000 requirement objects. Each object accepts the same fields as the Create Requirement body.position(integer): optional. 1-based position at which to insert the imported requirements. Existing requirements at or after this position are shifted down. When omitted, the requirements are appended to the end of the list.
Get Requirement
GET https://api.epsilon3.io/v1/testing/requirements/:id
Retrieve a single requirement using its ID.
Parameters
id(path parameter): ID of the requirement being retrieved.
Example Request URL
GET https://api.epsilon3.io/v1/testing/requirements/6be45049-ed63-4d53-b839-a0a719f94be9
Example Response Body
{
"id": "6be45049-ed63-4d53-b839-a0a719f94be9",
"custom_id": "A-100",
"position": 1,
"text": "The aircraft should be a commercial passenger airplane designed for carrying passengers and cargo.",
"priority": {
"id": 1,
"name": "High",
"color": "red-200"
},
"type": "secondary",
"notes": "This is not a military aircraft.",
"cases": [], // list of associated test points
"source_links": [], // list of associated requirements this requirement is a source of
"target_links": [], // list of associated requirements that target this requirement
"attachments": [], // list of attachment metadata
}
Update Requirement
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v1/testing/requirements/6be45049-ed63-4d53-b839-a0a719f94be9',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
'notes': 'Updated after design review.',
'priority_id': 2
}
)
curl -X PATCH \
https://api.epsilon3.io/v1/testing/requirements/6be45049-ed63-4d53-b839-a0a719f94be9 \
-u "$EPSILON3_API_KEY": \
-H "Content-Type: application/json" \
-d '{"notes":"Updated after design review.","priority_id":2}'
$apiUrl = "https://api.epsilon3.io/v1/testing/requirements/6be45049-ed63-4d53-b839-a0a719f94be9"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
"Content-Type" = "application/json"
}
$body = @{
notes = "Updated after design review."
priority_id = 2
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
PATCH https://api.epsilon3.io/v1/testing/requirements/:id
Partially update a requirement using its ID. Only the fields included in the request body are changed; any field that is omitted is left unchanged. Returns a 204 No Content on success.
Parameters
id(path parameter): ID of the requirement being updated.
Body Parameters
All body parameters are optional. Every field accepted by the Create Requirement body can be supplied here, including custom_id, text, position, priority_id, status_id, type, notes, case_ids, source_links, target_links, project_id, version, attachments, and details. Nullable fields (such as text, priority_id, type, notes, project_id, and version) can be set to null to clear them.
Example Request URL
PATCH https://api.epsilon3.io/v1/testing/requirements/6be45049-ed63-4d53-b839-a0a719f94be9
Delete Requirement
Sample Request
import requests
import os
requests.delete(
url='https://api.epsilon3.io/v1/testing/requirements/6be45049-ed63-4d53-b839-a0a719f94be9',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X DELETE \
https://api.epsilon3.io/v1/testing/requirements/6be45049-ed63-4d53-b839-a0a719f94be9 \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/testing/requirements/6be45049-ed63-4d53-b839-a0a719f94be9"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Delete -Headers $headers
DELETE https://api.epsilon3.io/v1/testing/requirements/:id
Delete a single requirement using its ID. Returns a 204 No Content on success.
Parameters
id(path parameter): ID of the requirement being deleted.
Example Request URL
DELETE https://api.epsilon3.io/v1/testing/requirements/6be45049-ed63-4d53-b839-a0a719f94be9
List Requirement Versions
GET https://api.epsilon3.io/v1/testing/requirements/:id/versions
Retrieve all the versions for a requirement.
Parameters
id(path parameter): ID of the requirement being retrieved.
Example Request URL
GET https://api.epsilon3.io/v1/testing/requirements/6be45049-ed63-4d53-b839-a0a719f94be9/versions
Example Response Body
{
"data": [
{
"id": 123,
"timestamp": "2024-01-24T21:00:39.421Z",
"record":
{
"id": "6be45049-ed63-4d53-b839-a0a719f94be9",
"custom_id": "A-100",
"position": 1,
"text": "The aircraft should be a commercial passenger airplane designed for carrying passengers and cargo.",
"priority": {
"id": 1,
"name": "High",
"color": "red-200"
},
"type": "secondary",
"notes": "This is not a military aircraft."
}
},
...
]
}
Get Requirement Version
GET https://api.epsilon3.io/v1/testing/requirements/:id/versions/:versionId
Retrieve a single version for a requirement.
Parameters
id(path parameter): ID of the requirement being retrieved.versionId(path parameter): ID of the version being retrieved.
Example Request URL
GET https://api.epsilon3.io/v1/testing/requirements/6be45049-ed63-4d53-b839-a0a719f94be9/versions/123
Example Response Body
{
"id": 123,
"timestamp": "2024-01-24T21:00:39.421Z",
"record":
{
"id": "6be45049-ed63-4d53-b839-a0a719f94be9",
"custom_id": "A-100",
"position": 1,
"text": "The aircraft should be a commercial passenger airplane designed for carrying passengers and cargo.",
"priority": {
"id": 1,
"name": "High",
"color": "red-200"
},
"type": "secondary",
"notes": "This is not a military aircraft."
}
}
Get Requirement Metadata Options
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v1/testing/requirements/metadata-options',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v1/testing/requirements/metadata-options \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v1/testing/requirements/metadata-options"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v1/testing/requirements/metadata-options
Retrieve the priorities, statuses, types, and links available for requirements in your workspace. Useful for resolving the priority_id and status_id values accepted when creating a requirement.
Example Response Body
{
"priorities": [
{
"id": 1,
"name": "High",
"color": "red-200"
}
],
"statuses": [
{
"id": 1,
"name": "Active",
"color": "green-200"
}
],
"types": [
"secondary"
],
"links": [
{
"id": 1,
"name": "derives",
"source": "6be45049-ed63-4d53-b839-a0a719f94be9",
"target": "f2161547-615e-4959-a989-91f3627d20d1"
}
]
}
Users API
Supports managing user role assignments within your workspace. Roles can be assigned at the workspace level or scoped to individual projects.
List Users
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v2/users/summary',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
params={
"include-project-roles": "true"
}
)
curl -X GET \
"https://api.epsilon3.io/v2/users/summary?include-project-roles=true" \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v2/users/summary?include-project-roles=true"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v2/users/summary
Retrieve a list of all users in your workspace, including their workspace roles and project counts.
Query Parameters
include-project-roles: (optional) Whentrue, include each user's project roles underroles.project. Defaults tofalse, in which caseroles.projectis omitted.
Example Response Body
{
"users": [
{
"user_id": "user_abc123",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"status": "active",
"roles": {
"team": [
{
"id": "1001",
"name": "Admin",
"code": "ADMIN",
"is_paid": true
}
],
"project": {
"project_abc123": [
{
"id": "1002",
"name": "Operator",
"code": "OPERATOR",
"is_paid": false
}
]
}
},
"project_counts": {
"parent_project_count": 3,
"sub_project_count": 5
}
}
]
}
Response Body
{ "users": [
{
"user_id": string,
"email": string,
"first_name": string (nullable),
"last_name": string (nullable),
"status": string, ( "active" | "inactive" | "invited" )
"roles": {
"team": [
{
"id": string,
"name": string,
"code": string,
"is_paid": boolean
}
],
"project": { (only present when include-project-roles=true)
"<project_id>": [
{
"id": string,
"name": string,
"code": string,
"is_paid": boolean
}
]
}
},
"project_counts": {
"parent_project_count": number,
"sub_project_count": number
}
}
]}
List Project Users
Sample Request
import requests
import os
project_id = "project_abc123"
requests.get(
url=f'https://api.epsilon3.io/v2/projects/{project_id}/users',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v2/projects/project_abc123/users \
-u "$EPSILON3_API_KEY":
$projectId = "project_abc123"
$apiUrl = "https://api.epsilon3.io/v2/projects/$projectId/users"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v2/projects/:projectId/users
Retrieve all users who have roles in the specified project.
Example Response Body
{
"users": [
{
"user_id": "user_abc123",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"status": "active",
"roles": {
"team": [
{
"id": "1001",
"name": "Admin",
"code": "ADMIN",
"is_paid": true
}
],
"project": {
"project_abc123": [
{
"id": "1002",
"name": "Operator",
"code": "OPERATOR",
"is_paid": false
}
]
}
}
}
]
}
Response Body
{ "users": [
{
"user_id": string,
"email": string,
"first_name": string (nullable),
"last_name": string (nullable),
"status": string, ( "active" | "inactive" | "invited" )
"roles": {
"team": [
{
"id": string,
"name": string,
"code": string,
"is_paid": boolean
}
],
"project": {
"<project_id>": [
{
"id": string,
"name": string,
"code": string,
"is_paid": boolean
}
]
}
}
}
]}
Add User Roles
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v2/users/roles/add',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"user_roles": [
{
"user_id": "user_xyz789",
"workspace_role_ids": ["role_id_1", "role_id_2"],
"projects": {
"project_abc123": ["role_id_3"]
}
}
]
}
)
curl -X POST \
https://api.epsilon3.io/v2/users/roles/add \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"user_roles": [
{
"user_id": "user_xyz789",
"workspace_role_ids": ["role_id_1", "role_id_2"],
"projects": {
"project_abc123": ["role_id_3"]
}
}
]
}'
$apiUrl = "https://api.epsilon3.io/v2/users/roles/add"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"user_roles" = @(
@{
"user_id" = "user_xyz789"
"workspace_role_ids" = @("role_id_1", "role_id_2")
"projects" = @{
"project_abc123" = @("role_id_3")
}
}
)
} | ConvertTo-Json -Depth 4
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"user_roles": [
{
"user_id": string,
"workspace_role_ids": [string] (optional),
"projects": { "<project_id>": [string] } (optional)
}
]
}
POST https://api.epsilon3.io/v2/users/roles/add
Add roles to one or more users. Each entry in user_roles specifies a user and the roles to add. Roles already assigned to a user are silently skipped.
| Field | Type | Description |
|---|---|---|
user_roles |
array | A non-empty array of user role assignments. |
user_roles[].user_id |
string | The ID of the user to update. |
user_roles[].workspace_role_ids |
array of strings | (Optional) Workspace-level role IDs to add. |
user_roles[].projects |
object | (Optional) An object mapping project IDs to arrays of role IDs to add for that project. |
Remove User Roles
Sample Request
import requests
import os
requests.post(
url='https://api.epsilon3.io/v2/users/roles/remove',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"user_roles": [
{
"user_id": "user_xyz789",
"workspace_role_ids": ["role_id_1"],
"projects": {
"project_abc123": ["role_id_3"]
}
}
]
}
)
curl -X POST \
https://api.epsilon3.io/v2/users/roles/remove \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"user_roles": [
{
"user_id": "user_xyz789",
"workspace_role_ids": ["role_id_1"],
"projects": {
"project_abc123": ["role_id_3"]
}
}
]
}'
$apiUrl = "https://api.epsilon3.io/v2/users/roles/remove"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"user_roles" = @(
@{
"user_id" = "user_xyz789"
"workspace_role_ids" = @("role_id_1")
"projects" = @{
"project_abc123" = @("role_id_3")
}
}
)
} | ConvertTo-Json -Depth 4
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $body
Request Body
{
"user_roles": [
{
"user_id": string,
"workspace_role_ids": [string] (optional),
"projects": { "<project_id>": [string] } (optional)
}
]
}
POST https://api.epsilon3.io/v2/users/roles/remove
Remove roles from one or more users. Each entry in user_roles specifies a user and the roles to remove. Roles not currently assigned to a user are silently skipped.
| Field | Type | Description |
|---|---|---|
user_roles |
array | A non-empty array of user role removals. |
user_roles[].user_id |
string | The ID of the user to update. |
user_roles[].workspace_role_ids |
array of strings | (Optional) Workspace-level role IDs to remove. |
user_roles[].projects |
object | (Optional) An object mapping project IDs to arrays of role IDs to remove for that project. |
Get User Details
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v2/users/user_abc123/details',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v2/users/user_abc123/details \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v2/users/user_abc123/details"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v2/users/<user_id>/details
Retrieve a user's profile, including the built-in system fields and the values of custom fields that are visible to all users.
Example Response Body
{
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@example.com",
"phone": "+15551234567",
"job_title": "Flight Controller",
"team_name": "Operations",
"location": "Houston",
"timezone": "America/Chicago",
"work_schedule": null,
"details": [
{
"id": "field_abc123",
"field_id": "badge-number",
"name": "Badge Number",
"type": "Text",
"value": { "recorded": "B-1024" }
}
]
}
Each entry in details is a CustomFieldValue. Only custom fields configured as visible to all users appear here; restricted fields are available exclusively through the personnel details endpoints.
Get My Details
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v2/users/me/details',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v2/users/me/details \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v2/users/me/details"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v2/users/me/details
Retrieve your own profile. The response matches Get User Details with the addition of your emergency contact fields (emergency_contact_name, emergency_contact_relationship, emergency_contact_phone).
Update My Details
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v2/users/me/details',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"phone": "+15559876543",
"emergency_contact_name": "John Doe"
}
)
curl -X PATCH \
https://api.epsilon3.io/v2/users/me/details \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"phone": "+15559876543",
"emergency_contact_name": "John Doe"
}'
$apiUrl = "https://api.epsilon3.io/v2/users/me/details"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"phone" = "+15559876543"
"emergency_contact_name" = "John Doe"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
PATCH https://api.epsilon3.io/v2/users/me/details
Update your own profile fields. All fields are optional; only the fields provided are changed.
| Field | Type | Description |
|---|---|---|
first_name |
string | (Optional) Your first name. |
last_name |
string | (Optional) Your last name. |
phone |
string | (Optional) Your contact phone number. |
job_title |
string | (Optional) Your job title. |
team_name |
string | (Optional) The name of your team. |
location |
string | (Optional) Your work location. |
timezone |
string | (Optional) Your IANA timezone, e.g. America/Chicago. |
emergency_contact_name |
string | (Optional) Emergency contact name. |
emergency_contact_relationship |
string | (Optional) Emergency contact relationship. |
emergency_contact_phone |
string | (Optional) Emergency contact phone number. |
work_schedule |
object | (Optional) Weekly work schedule: { "days": { "monday": { "start_time", "end_time" }, ... } }. |
Custom field values cannot be updated through this endpoint; they are managed by administrators through Update User Details and Update Personnel Details.
Update User Details
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v2/users/user_abc123/details',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"job_title": "Flight Controller",
"location": "Houston",
"details": [
{"id": "field_abc123", "value": {"recorded": "B-2048"}}
]
}
)
curl -X PATCH \
https://api.epsilon3.io/v2/users/user_abc123/details \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"job_title": "Flight Controller",
"location": "Houston",
"details": [
{"id": "field_abc123", "value": {"recorded": "B-2048"}}
]
}'
$apiUrl = "https://api.epsilon3.io/v2/users/user_abc123/details"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"job_title" = "Flight Controller"
"location" = "Houston"
"details" = @(
@{ "id" = "field_abc123"; "value" = @{ "recorded" = "B-2048" } }
)
} | ConvertTo-Json -Depth 4
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
PATCH https://api.epsilon3.io/v2/users/<user_id>/details
Update another user's public profile. All fields are optional; only the fields provided are changed.
| Field | Type | Description |
|---|---|---|
first_name |
string | (Optional) The user's first name. |
last_name |
string | (Optional) The user's last name. |
phone |
string | (Optional) The user's contact phone number. |
job_title |
string | (Optional) The user's job title. |
team_name |
string | (Optional) The name of the user's team. |
location |
string | (Optional) The user's work location. |
timezone |
string | (Optional) The user's IANA timezone, e.g. America/Chicago. |
details |
array | (Optional) Values to record for custom fields visible to all users. |
details[].id |
string | The custom field's id, as returned in the details array of Get User Details. |
details[].value.recorded |
varies | The value to record — see CustomFieldValue for the shape per field type. Pass null to clear. |
Each entry in details must reference a field on the visible-to-all-users form; a mismatch fails the whole request with HTTP 422 and nothing is saved. Personnel file fields and restricted custom fields are managed through Update Personnel Details.
Update Users Details
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v2/users/details',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"user_ids": ["user_abc123", "user_xyz789"],
"job_title": "Flight Controller",
"location": "Houston"
}
)
curl -X PATCH \
https://api.epsilon3.io/v2/users/details \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"user_ids": ["user_abc123", "user_xyz789"],
"job_title": "Flight Controller",
"location": "Houston"
}'
$apiUrl = "https://api.epsilon3.io/v2/users/details"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"user_ids" = @("user_abc123", "user_xyz789")
"job_title" = "Flight Controller"
"location" = "Houston"
} | ConvertTo-Json
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
PATCH https://api.epsilon3.io/v2/users/details
Update profile fields on one or more users at once. Only the fields provided are changed, and the same values are applied to every user in user_ids.
| Field | Type | Description |
|---|---|---|
user_ids |
array of strings | The IDs of the users to update. |
job_title |
string | (Optional) Job title to apply. |
location |
string | (Optional) Work location to apply. |
team_name |
string | (Optional) Team name to apply. |
timezone |
string | (Optional) IANA timezone to apply. |
Get Personnel Details
Sample Request
import requests
import os
requests.get(
url='https://api.epsilon3.io/v2/users/user_abc123/personnel-details',
auth=(os.environ.get('EPSILON3_API_KEY'), '')
)
curl -X GET \
https://api.epsilon3.io/v2/users/user_abc123/personnel-details \
-u "$EPSILON3_API_KEY":
$apiUrl = "https://api.epsilon3.io/v2/users/user_abc123/personnel-details"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Authorization" = "Basic $authHeader"
}
Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
GET https://api.epsilon3.io/v2/users/<user_id>/personnel-details
Retrieve a user's full personnel file: the public profile fields, the restricted system fields (employment and compensation data, emergency contacts), and the values of all custom fields — both those visible to all users (details) and those restricted to the Personnel File (personnel_details).
Example Response Body
{
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@example.com",
"phone": "+15551234567",
"job_title": "Flight Controller",
"team_name": "Operations",
"location": "Houston",
"timezone": "America/Chicago",
"employee_id": "EMP-1024",
"manager_id": "EMP-1001",
"start_date": "2024-01-15",
"end_date": null,
"base_pay": "100000",
"bonus_pay": "10000",
"other_pay": null,
"pay_currency": "USD",
"emergency_contact_name": "John Doe",
"emergency_contact_relationship": "Spouse",
"emergency_contact_phone": "+15559876543",
"details": [
{
"id": "field_abc123",
"field_id": "badge-number",
"name": "Badge Number",
"type": "Text",
"value": { "recorded": "B-1024" }
}
],
"personnel_details": [
{
"id": "field_def456",
"field_id": "clearance-level",
"name": "Clearance Level",
"type": "Text",
"value": { "recorded": "Secret" }
}
]
}
Each entry in details and personnel_details is a CustomFieldValue.
Update Personnel Details
Sample Request
import requests
import os
requests.patch(
url='https://api.epsilon3.io/v2/users/user_abc123/personnel-details',
auth=(os.environ.get('EPSILON3_API_KEY'), ''),
json={
"employee_id": "EMP-1024",
"start_date": "2024-01-15",
"details": [
{"id": "field_abc123", "value": {"recorded": "B-2048"}}
],
"personnel_details": [
{"id": "field_def456", "value": {"recorded": "Top Secret"}}
]
}
)
curl -X PATCH \
https://api.epsilon3.io/v2/users/user_abc123/personnel-details \
-H 'Content-Type: application/json' \
-u "$EPSILON3_API_KEY": \
-d '{
"employee_id": "EMP-1024",
"start_date": "2024-01-15",
"details": [
{"id": "field_abc123", "value": {"recorded": "B-2048"}}
],
"personnel_details": [
{"id": "field_def456", "value": {"recorded": "Top Secret"}}
]
}'
$apiUrl = "https://api.epsilon3.io/v2/users/user_abc123/personnel-details"
$apiKey = $env:EPSILON3_API_KEY
$authHeader = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$apiKey:"))
$headers = @{
"Content-Type" = "application/json"
"Authorization" = "Basic $authHeader"
}
$body = @{
"employee_id" = "EMP-1024"
"start_date" = "2024-01-15"
"details" = @(
@{ "id" = "field_abc123"; "value" = @{ "recorded" = "B-2048" } }
)
"personnel_details" = @(
@{ "id" = "field_def456"; "value" = @{ "recorded" = "Top Secret" } }
)
} | ConvertTo-Json -Depth 4
Invoke-RestMethod -Uri $apiUrl -Method Patch -Headers $headers -Body $body
PATCH https://api.epsilon3.io/v2/users/<user_id>/personnel-details
Update a user's personnel file. All fields are optional; only the fields provided are changed. Custom field values visible to all users are recorded via details; this is also the only endpoint that records restricted custom field values, via personnel_details.
| Field | Type | Description |
|---|---|---|
first_name, last_name, phone, job_title, team_name, location, timezone |
string | (Optional) Public profile fields. |
employee_id, manager_id |
string | (Optional) Employment identifiers. |
start_date, end_date |
string | (Optional) Employment dates, YYYY-MM-DD. |
base_pay, bonus_pay, other_pay, pay_currency |
string | (Optional) Compensation fields. |
emergency_contact_name, emergency_contact_relationship, emergency_contact_phone |
string | (Optional) Emergency contact fields. |
details |
array | (Optional) Values to record for custom fields visible to all users. |
personnel_details |
array | (Optional) Values to record for custom fields restricted to the Personnel File. |
details[].id, personnel_details[].id |
string | The custom field's id, as returned in the corresponding array of Get Personnel Details. |
details[].value.recorded, personnel_details[].value.recorded |
varies | The value to record — see CustomFieldValue for the shape per field type. Pass null to clear. |
Each entry in details must reference a field on the visible-to-all-users form, and each entry in personnel_details must reference a field on the restricted Personnel form; a mismatch fails the whole request with HTTP 422 and nothing is saved.


