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"
responses:
200:
description: "Success response"
schema:
type: object
properties:
id:
type: string
404:
description: "Not found"
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) |
responses | object | No | - | Response definitions for Swagger documentation. See API Responses |
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, email) |
enum | array | No | List of allowed values for the parameter. Shown as a dropdown in Swagger UI |
schema | object | No | Detailed schema definition for body inputs. See Body Input Schema |
Input Example
inputs:
- name: "orderId"
type: "integer"
props:
in: "path"
required: true
description: "The order ID"
- name: "status"
type: "string"
props:
in: "query"
required: false
description: "Filter by order status"
enum: ["Draft", "Active", "InTransit", "Delivered", "Cancelled"]
- 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"
Body Input Schema
For body inputs, you can define a detailed schema that appears in Swagger UI. This helps API consumers understand the expected request body structure.
inputs:
- name: "payload"
type: "object"
props:
in: "body"
required: true
description: "Order creation payload"
schema:
required:
- customerName
- originCity
properties:
customerName:
type: string
description: "Customer full name"
originCity:
type: string
description: "City of origin"
notes:
type: string
description: "Optional notes"
priority:
type: string
enum: ["low", "normal", "high", "urgent"]
description: "Order priority level"
The schema object supports:
| Property | Type | Description |
|---|---|---|
properties | object | Map of property names to their schema definitions |
required | array | List of required property names |
Each property in properties supports:
| Property | Type | Description |
|---|---|---|
type | string | Data type: string, integer, number, boolean, object, array |
format | string | Format hint (e.g., uuid, date-time, email) |
description | string | Property description |
enum | array | Allowed values |
properties | object | Nested properties (for type: object) |
required | array | Required nested properties (for type: object) |
items | object | Item schema (for type: array) |
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. Use 204 for no-content responses |
outputs:
- name: "response"
mapping: "fetchData.result"
- name: "statusCode"
mapping: "fetchData.code"
When statusCode is 204, the endpoint returns an empty response body (HTTP 204 No Content), regardless of the response output value.
API Responses
The api.responses block defines response schemas for Swagger documentation per HTTP status code. This is the recommended way to document your API responses because it:
- Supports multiple status codes (200, 201, 204, 400, 404, etc.)
- Keeps documentation concerns in the
apiblock, separate from runtimeoutputs - Supports no-content responses (no
schema= no body in Swagger) - Aligns with OpenAPI specification structure
Basic Example
api:
path: "/orders/{orderId}"
method: "GET"
responses:
200:
description: "Order details"
schema:
type: object
properties:
id:
type: string
format: uuid
orderNumber:
type: string
total:
type: number
404:
description: "Order not found"
schema:
type: object
properties:
error:
type: string
outputs:
- name: "response"
mapping: "fetchOrder.result"
- name: "statusCode"
mapping: "fetchOrder.code"
No Content Response (204)
For DELETE or other operations that return no body:
api:
path: "/orders/{orderId}"
method: "DELETE"
responses:
204:
description: "Order deleted successfully"
404:
description: "Order not found"
schema:
type: object
properties:
error:
type: string
outputs:
- name: "statusCode"
mapping: "deleteOrder.code"
When a response has no schema, Swagger shows it as having no response body.
Paginated List Response
api:
path: "/orders"
method: "GET"
responses:
200:
description: "Paginated order list"
schema:
type: object
required:
- items
- totalCount
properties:
items:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
orderNumber:
type: string
status:
type: string
enum: ["Draft", "Active", "InTransit", "Delivered"]
totalCount:
type: integer
description: "Total number of matching records"
Array Response
For endpoints that return a plain array:
api:
path: "/tags"
method: "GET"
responses:
200:
description: "List of tags"
schema:
type: array
items:
type: object
properties:
id:
type: string
label:
type: string
Nested Object Response
api:
path: "/orders/{orderId}"
method: "GET"
responses:
200:
description: "Order with customer details"
schema:
type: object
required:
- id
properties:
id:
type: string
format: uuid
customer:
type: object
description: "Customer information"
required:
- name
properties:
name:
type: string
email:
type: string
format: email
address:
type: object
properties:
city:
type: string
state:
type: string
zip:
type: string
Response Config Reference
Each entry in api.responses is keyed by HTTP status code:
| Property | Type | Required | Description |
|---|---|---|---|
description | string | No | Description shown in Swagger UI |
schema | object | No | Response body schema. Omit for no-content responses (e.g., 204) |
The schema object supports the same recursive structure as input schemas:
| Property | Type | Description |
|---|---|---|
type | string | Data type: object, array, string, integer, number, boolean |
format | string | Format hint (e.g., uuid, date-time, email) |
description | string | Property description |
enum | array | Allowed values |
properties | object | Nested properties (for type: object) |
required | array | Required property names (for type: object) |
items | object | Item schema (for type: array) |
Legacy: Response Schema via Output Props
For backward compatibility, you can also define a response schema using props on the response output. This approach only supports a single 200 response:
outputs:
- name: "response"
mapping: "fetchOrder.result"
props:
type: object
description: "Order details"
schema:
properties:
id:
type: string
format: uuid
orderNumber:
type: string
If both api.responses and outputs[response].props are defined, api.responses takes precedence.
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
responses:
200:
description: "Order details"
schema:
type: object
properties:
orderId:
type: integer
orderNumber:
type: string
orderDate:
type: string
format: date-time
orderStatus:
type: object
properties:
orderStatusName:
type: string
customer:
type: object
properties:
name:
type: string
email:
type: string
format: email
401:
description: "Invalid API key"
schema:
type: object
properties:
error:
type: string
404:
description: "Order not found"
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
responses:
201:
description: "Tracking event created"
schema:
type: object
properties:
message:
type: string
trackingEventId:
type: integer
400:
description: "Invalid tracking event data"
schema:
type: object
properties:
error:
type: string
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"
schema:
required:
- eventCode
- eventDate
properties:
eventCode:
type: string
description: "Tracking event code"
enum: ["PICKED_UP", "IN_TRANSIT", "OUT_FOR_DELIVERY", "DELIVERED", "EXCEPTION"]
eventDate:
type: string
format: date-time
description: "Date and time of the event"
description:
type: string
description: "Human-readable event description"
location:
type: string
description: "Location where the event occurred"
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
responses:
200:
description: "Paginated list of orders"
schema:
type: object
properties:
items:
type: array
items:
type: object
properties:
orderId:
type: integer
orderNumber:
type: string
orderDate:
type: string
format: date-time
orderStatus:
type: object
properties:
orderStatusName:
type: string
totalCount:
type: integer
description: "Total number of matching orders"
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"
enum: ["Draft", "Pending", "In Transit", "Delivered", "Cancelled"]
- 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
responses:
200:
description: "Shipment tracking information"
schema:
type: object
properties:
trackingNumber:
type: string
orderStatus:
type: object
properties:
orderStatusName:
type: string
orderEvents:
type: array
items:
type: object
properties:
trackingEvent:
type: object
properties:
eventDate:
type: string
format: date-time
eventDefinition:
type: object
properties:
eventCode:
type: string
description:
type: string
location:
type: string
404:
description: "Shipment not found"
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.)