Workflow: Public API Type
A Public API workflow exposes custom REST endpoints that external systems, partners, or frontend applications can call. Each workflow defines a single HTTP endpoint with its own path, method, authentication, rate limiting, and business logic.
Public API workflows are ideal when you need to:
- Expose data or actions to external partners without giving them full system access
- Build custom REST APIs backed by workflow logic (queries, transformations, integrations)
- Create webhooks for third-party services with input validation and typed parameters
- Serve dynamic content from your TMS data for portals or mobile apps
Base URL
All Public API endpoints are served under:
/public-api/v1/{orgUniqueId}/{path}
Where {orgUniqueId} is your organization's unique identifier (UUID format).
Swagger / OpenAPI Documentation
Each organization gets auto-generated Swagger documentation based on its active Public API workflows.
Swagger Endpoints
| URL | Description |
|---|---|
/public-api/v1/{orgUniqueId}/swagger/ | Lists all available API document groups |
/public-api/v1/{orgUniqueId}/swagger/{documentName} | Interactive Swagger UI for a document group |
/public-api/v1/{orgUniqueId}/swagger/{documentName}/swagger.json | OpenAPI 3.0 JSON specification |
Document Groups
Workflows are grouped into Swagger documents using the api.document property. If not specified, workflows default to the "public" document group.
api:
document: "partners" # This endpoint appears in the "partners" Swagger doc
To view the partners Swagger UI, navigate to:
/public-api/v1/{orgUniqueId}/swagger/partners
YAML Structure
A Public API workflow uses the standard workflow manifest with an additional api section:
api:
path: "/your/endpoint/{param}"
method: "GET"
authentication: "none"
# ... additional API config
workflow:
name: "My Public Endpoint"
workflowId: "00000000-0000-0000-0000-000000000000"
workflowType: "PublicApi"
executionMode: "Sync" # Required: must be Sync
isActive: true
inputs:
- name: "param"
type: "string"
props:
in: "path"
required: true
outputs:
- name: "response" # Required
mapping: "stepName.result"
- name: "statusCode" # Optional
mapping: "stepName.code"
activities:
- name: myActivity
steps:
- task: "SomeTask@1"
name: "stepName"
# ...
API Configuration
The api section defines the HTTP endpoint configuration.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
path | string | Yes | - | URL path (must start with /). Supports path parameters with {paramName} syntax |
method | string | Yes | - | HTTP method: GET, POST, PUT, PATCH, or DELETE |
authentication | string | No | bearer | Authentication method: none, bearer, or apiKey |
summary | string | No | - | Short summary shown in Swagger UI |
description | string | No | - | Detailed description shown in Swagger UI |
operationId | string | No | auto-generated | Unique operation ID for the OpenAPI spec |
document | string | No | public | Swagger document group name |
category | string | No | - | Category tag for grouping endpoints in Swagger UI |
rateLimit | object | No | - | Rate limiting configuration |
rateLimit.perSecond | number | No | 0 | Max requests per second per client IP |
rateLimit.perMinute | number | No | 0 | Max requests per minute per client IP |
timeout | number | No | 60 | Workflow execution timeout in seconds |
maxBodySize | number | No | 1048576 | Maximum request body size in bytes (default 1MB) |
Authentication
Public API workflows support three authentication methods:
No Authentication
api:
authentication: "none"
The endpoint is publicly accessible without any credentials.
Bearer Token (Default)
api:
authentication: "bearer"
Requires a valid JWT Bearer token in the Authorization header:
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
If authentication is omitted, bearer is used by default.
API Key
api:
authentication: "apiKey"
Requires the X-API-Key header:
X-API-Key: your-api-key-here
The API key value is passed to the workflow as a header input. Your workflow is responsible for validating the key (e.g., by looking it up in a configuration or database).
Inputs
Inputs define the parameters your endpoint accepts. Each input specifies where the value comes from using the props.in property.
Input Locations
props.in | Source | Description |
|---|---|---|
path | URL path | Extracted from path parameters (e.g., {orderId} in /orders/{orderId}) |
query | Query string | Extracted from URL query parameters (e.g., ?status=active) |
header | HTTP headers | Extracted from request headers |
body | Request body | Parsed from the JSON request body. Only one body input is allowed per endpoint |
Input Types
| Type | Description |
|---|---|
string | Text value (default) |
integer / int | Integer number |
number / decimal / float / double | Decimal number |
boolean / bool | Boolean value |
object | JSON object (for body inputs) |
Input Props
| Property | Type | Required | Description |
|---|---|---|---|
in | string | Yes | Input location: path, query, header, or body |
required | boolean | No | Whether the parameter is required. Path parameters are always required |
description | string | No | Description shown in Swagger UI |
format | string | No | Format hint for Swagger UI (e.g., uuid, date-time) |
Input Example
inputs:
- name: "orderId"
type: "integer"
props:
in: "path"
required: true
description: "The order ID"
- name: "includeDetails"
type: "boolean"
props:
in: "query"
required: false
description: "Include full order details"
- name: "X-API-Key"
type: "string"
props:
in: "header"
required: true
description: "API key for authentication"
- name: "payload"
type: "object"
props:
in: "body"
required: true
description: "Request payload"
Outputs
Public API workflows must define a response output. An optional statusCode output controls the HTTP status code.
| Output Name | Required | Description |
|---|---|---|
response | Yes | The response body returned to the caller. Can be any JSON-serializable value |
statusCode | No | HTTP status code (integer). Defaults to 200 if not set |
outputs:
- name: "response"
mapping: "fetchData.result"
- name: "statusCode"
mapping: "fetchData.code"
Request Metadata
In addition to the defined inputs, the workflow receives a request variable with HTTP request metadata:
| Property | Description |
|---|---|
request.headers | Dictionary of all HTTP request headers |
request.body | Raw request body as a string |
request.method | HTTP method (GET, POST, etc.) |
request.path | Normalized request path |
request.remoteIpAddress | Client IP address |
Rate Limiting
Rate limiting is per-client-IP using a sliding window algorithm. Configure it in the api.rateLimit section:
api:
rateLimit:
perSecond: 10
perMinute: 100
When the rate limit is exceeded, the endpoint returns 429 Too Many Requests.
Client IP is resolved from (in order): CF-Connecting-IP, X-Forwarded-For, X-Real-IP, or the connection's remote IP.
Error Responses
| Status Code | Condition |
|---|---|
200 | Success (or custom code via statusCode output) |
400 | Input validation failed (missing required params, type mismatch) |
401 | Authentication failed (missing or invalid credentials) |
404 | Organization not found or no matching endpoint |
413 | Request body exceeds maxBodySize |
429 | Rate limit exceeded |
504 | Workflow execution timed out |
500 | Unexpected server error |
Error response format:
{
"error": "Description of the error."
}
For input validation errors:
{
"error": "Input validation failed.",
"details": {
"orderId": "Required parameter 'orderId' is missing.",
"quantity": "Cannot convert 'abc' to integer."
}
}
Validation Rules
Public API workflows are validated at save time. The following rules apply:
| Code | Rule |
|---|---|
PAPI_001 | api section is required |
PAPI_002 | api.path is required |
PAPI_003 | api.method is required |
PAPI_004 | api.method must be GET, POST, PUT, PATCH, or DELETE |
PAPI_005 | api.authentication must be none, bearer, or apiKey (if provided) |
PAPI_006 | executionMode must be Sync |
PAPI_007 | Every {param} in the path must have a matching input with in: path |
PAPI_008 | At most one input can have in: body |
PAPI_010 | Path must start with / and must not contain .. |
PAPI_011 | Outputs must include a response output |
PAPI_012 | Warning: body input on GET or DELETE methods |
Examples
Example 1: GET Endpoint - Fetch Order by ID
A simple GET endpoint that retrieves an order by ID and returns it as JSON.
api:
path: "/orders/{orderId}"
method: "GET"
summary: "Get order by ID"
description: "Returns order details for the given order ID"
operationId: "getOrderById"
document: "partners"
category: "Orders"
authentication: "apiKey"
rateLimit:
perSecond: 10
perMinute: 100
workflow:
name: "Public API / Get Order"
workflowId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
workflowType: "PublicApi"
executionMode: "Sync"
isActive: true
inputs:
- name: "orderId"
type: "integer"
props:
in: "path"
required: true
description: "Order ID"
- name: "X-API-Key"
type: "string"
props:
in: "header"
required: true
description: "Partner API key"
outputs:
- name: "response"
mapping: "fetchOrder.result"
- name: "statusCode"
mapping: "checkAuth.statusCode"
activities:
- name: validateKey
steps:
- task: "Utilities/SetVariable@1"
name: "checkAuth"
inputs:
variables:
- name: "statusCode"
expression: "if([X-API-Key] = '{{ partnerApiKey }}', null, 401)"
- name: "authError"
expression: "if([X-API-Key] = '{{ partnerApiKey }}', null, 'Invalid API key')"
- name: fetchOrderData
conditions:
- expression: "[checkAuth.statusCode] = null"
steps:
- task: "Query/GraphQL@1"
name: "fetchOrder"
inputs:
query: |
query {
order(orderId: {{ orderId }}, organizationId: {{ organizationId }}) {
orderId
orderNumber
orderDate
orderStatus { orderStatusName }
customer { name, email }
shipper { contact { name } }
consignee { contact { name } }
}
}
outputs:
- name: "result"
mapping: "order"
Example request:
curl -X GET "https://your-domain.com/public-api/v1/{orgUniqueId}/orders/12345" \
-H "X-API-Key: your-partner-key"
Example response (200):
{
"orderId": 12345,
"orderNumber": "ORD-2025-0001",
"orderDate": "2025-01-15T10:30:00Z",
"orderStatus": {
"orderStatusName": "In Transit"
},
"customer": {
"name": "Acme Corp",
"email": "logistics@acme.com"
}
}
Example 2: POST Endpoint - Create Tracking Event
A POST endpoint that accepts a JSON payload to create a tracking event for an order.
api:
path: "/orders/{orderId}/tracking"
method: "POST"
summary: "Create tracking event"
description: "Submit a tracking event for the specified order"
document: "partners"
category: "Tracking"
authentication: "apiKey"
rateLimit:
perSecond: 5
perMinute: 60
timeout: 30
maxBodySize: 65536 # 64KB
workflow:
name: "Public API / Create Tracking Event"
workflowId: "b2c3d4e5-f6a7-8901-bcde-f23456789012"
workflowType: "PublicApi"
executionMode: "Sync"
isActive: true
inputs:
- name: "orderId"
type: "integer"
props:
in: "path"
required: true
description: "Order ID"
- name: "payload"
type: "object"
props:
in: "body"
required: true
description: "Tracking event data"
outputs:
- name: "response"
mapping: "buildResponse.result"
- name: "statusCode"
mapping: "buildResponse.code"
variables:
- name: "eventCode"
value: "{{ payload.eventCode }}"
- name: "eventDate"
value: "{{ payload.eventDate }}"
- name: "description"
value: "{{ payload.description }}"
- name: "location"
value: "{{ payload.location }}"
activities:
- name: createEvent
steps:
- task: "TrackingEvent/Create@1"
name: "createTracking"
inputs:
orderId: "{{ orderId }}"
eventCode: "{{ eventCode }}"
eventDate: "{{ eventDate }}"
description: "{{ description }}"
location: "{{ location }}"
outputs:
- name: "trackingEventId"
- task: "Utilities/SetVariable@1"
name: "buildResponse"
inputs:
variables:
- name: "result"
value:
message: "Tracking event created successfully"
trackingEventId: "{{ createTracking.trackingEventId }}"
- name: "code"
value: 201
Example request:
curl -X POST "https://your-domain.com/public-api/v1/{orgUniqueId}/orders/12345/tracking" \
-H "X-API-Key: your-partner-key" \
-H "Content-Type: application/json" \
-d '{
"eventCode": "DELIVERED",
"eventDate": "2025-06-15T14:30:00Z",
"description": "Package delivered to front door",
"location": "New York, NY"
}'
Example response (201):
{
"message": "Tracking event created successfully",
"trackingEventId": 98765
}
Example 3: GET Endpoint with Query Parameters
A list endpoint with pagination and filtering via query parameters.
api:
path: "/orders"
method: "GET"
summary: "List orders"
description: "Returns a paginated list of orders with optional filtering"
document: "partners"
category: "Orders"
authentication: "bearer"
rateLimit:
perSecond: 5
perMinute: 60
workflow:
name: "Public API / List Orders"
workflowId: "c3d4e5f6-a7b8-9012-cdef-345678901234"
workflowType: "PublicApi"
executionMode: "Sync"
isActive: true
inputs:
- name: "status"
type: "string"
props:
in: "query"
required: false
description: "Filter by order status name"
- name: "page"
type: "integer"
props:
in: "query"
required: false
description: "Page number (default: 1)"
- name: "pageSize"
type: "integer"
props:
in: "query"
required: false
description: "Page size (default: 20, max: 100)"
outputs:
- name: "response"
mapping: "fetchOrders.result"
activities:
- name: queryOrders
steps:
- task: "Query/GraphQL@1"
name: "fetchOrders"
inputs:
query: |
query {
orders(
organizationId: {{ organizationId }},
filter: { statusName: "{{ status? }}" },
skip: {{ (page? ?? 1 - 1) * (pageSize? ?? 20) }},
take: {{ pageSize? ?? 20 }}
) {
items {
orderId
orderNumber
orderDate
orderStatus { orderStatusName }
}
totalCount
}
}
outputs:
- name: "result"
mapping: "orders"
Example request:
curl -X GET "https://your-domain.com/public-api/v1/{orgUniqueId}/orders?status=In%20Transit&page=1&pageSize=10" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
Example 4: Public Endpoint (No Authentication)
A public endpoint that returns tracking information without requiring authentication.
api:
path: "/track/{trackingNumber}"
method: "GET"
summary: "Track shipment"
description: "Public tracking page - returns shipment status by tracking number"
document: "public"
category: "Tracking"
authentication: "none"
rateLimit:
perSecond: 20
perMinute: 200
workflow:
name: "Public API / Track Shipment"
workflowId: "d4e5f6a7-b8c9-0123-def0-456789012345"
workflowType: "PublicApi"
executionMode: "Sync"
isActive: true
inputs:
- name: "trackingNumber"
type: "string"
props:
in: "path"
required: true
description: "Shipment tracking number"
outputs:
- name: "response"
mapping: "buildResult.result"
- name: "statusCode"
mapping: "buildResult.code"
activities:
- name: lookup
steps:
- task: "Query/GraphQL@1"
name: "findOrder"
inputs:
query: |
query {
orders(
organizationId: {{ organizationId }},
filter: { trackingNumber: "{{ trackingNumber }}" },
take: 1
) {
items {
trackingNumber
orderStatus { orderStatusName }
orderEvents {
trackingEvent {
eventDate
eventDefinition { eventCode, description }
location
}
}
}
}
}
outputs:
- name: "orders"
mapping: "orders.items"
- task: "Utilities/SetVariable@1"
name: "buildResult"
inputs:
variables:
- name: "result"
expression: "if(count([findOrder.orders]) > 0, elementAt([findOrder.orders], 0), null)"
- name: "code"
expression: "if(count([findOrder.orders]) > 0, 200, 404)"
Route Matching
Routes are matched using the following rules:
- Method matching: The HTTP method must match exactly (case-insensitive)
- Path matching: Path templates like
/orders/{orderId}/items/{itemId}are converted to regex patterns with named capture groups - Specificity: Routes with more static segments take priority over parameterized routes. For example,
/orders/recentmatches before/orders/{orderId} - First match wins: When multiple routes match at the same specificity level, the first match is returned
Best Practices
- Use meaningful paths that follow REST conventions (e.g.,
/orders/{orderId},/orders/{orderId}/tracking) - Group related endpoints using the same
documentvalue for organized Swagger documentation - Set appropriate rate limits to protect your system from abuse
- Use
apiKeyauthentication for partner integrations where you control key distribution - Use
noneauthentication only for truly public endpoints (tracking pages, status checks) - Always validate inputs using required fields and type constraints
- Set a reasonable
timeoutbased on the expected workflow execution time - Return meaningful status codes via the
statusCodeoutput (201 for creates, 404 for not found, etc.)