Skip to main content

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

URLDescription
/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.jsonOpenAPI 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.

PropertyTypeRequiredDefaultDescription
pathstringYes-URL path (must start with /). Supports path parameters with {paramName} syntax
methodstringYes-HTTP method: GET, POST, PUT, PATCH, or DELETE
authenticationstringNobearerAuthentication method: none, bearer, or apiKey
summarystringNo-Short summary shown in Swagger UI
descriptionstringNo-Detailed description shown in Swagger UI
operationIdstringNoauto-generatedUnique operation ID for the OpenAPI spec
documentstringNopublicSwagger document group name
categorystringNo-Category tag for grouping endpoints in Swagger UI
rateLimitobjectNo-Rate limiting configuration
rateLimit.perSecondnumberNo0Max requests per second per client IP
rateLimit.perMinutenumberNo0Max requests per minute per client IP
timeoutnumberNo60Workflow execution timeout in seconds
maxBodySizenumberNo1048576Maximum request body size in bytes (default 1MB)
responsesobjectNo-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.inSourceDescription
pathURL pathExtracted from path parameters (e.g., {orderId} in /orders/{orderId})
queryQuery stringExtracted from URL query parameters (e.g., ?status=active)
headerHTTP headersExtracted from request headers
bodyRequest bodyParsed from the JSON request body. Only one body input is allowed per endpoint

Input Types

TypeDescription
stringText value (default)
integer / intInteger number
number / decimal / float / doubleDecimal number
boolean / boolBoolean value
objectJSON object (for body inputs)

Input Props

PropertyTypeRequiredDescription
instringYesInput location: path, query, header, or body
requiredbooleanNoWhether the parameter is required. Path parameters are always required
descriptionstringNoDescription shown in Swagger UI
formatstringNoFormat hint for Swagger UI (e.g., uuid, date-time, email)
enumarrayNoList of allowed values for the parameter. Shown as a dropdown in Swagger UI
schemaobjectNoDetailed 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:

PropertyTypeDescription
propertiesobjectMap of property names to their schema definitions
requiredarrayList of required property names

Each property in properties supports:

PropertyTypeDescription
typestringData type: string, integer, number, boolean, object, array
formatstringFormat hint (e.g., uuid, date-time, email)
descriptionstringProperty description
enumarrayAllowed values
propertiesobjectNested properties (for type: object)
requiredarrayRequired nested properties (for type: object)
itemsobjectItem schema (for type: array)

Outputs

Public API workflows must define a response output. An optional statusCode output controls the HTTP status code.

Output NameRequiredDescription
responseYesThe response body returned to the caller. Can be any JSON-serializable value
statusCodeNoHTTP 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 api block, separate from runtime outputs
  • 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:

PropertyTypeRequiredDescription
descriptionstringNoDescription shown in Swagger UI
schemaobjectNoResponse body schema. Omit for no-content responses (e.g., 204)

The schema object supports the same recursive structure as input schemas:

PropertyTypeDescription
typestringData type: object, array, string, integer, number, boolean
formatstringFormat hint (e.g., uuid, date-time, email)
descriptionstringProperty description
enumarrayAllowed values
propertiesobjectNested properties (for type: object)
requiredarrayRequired property names (for type: object)
itemsobjectItem 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:

PropertyDescription
request.headersDictionary of all HTTP request headers
request.bodyRaw request body as a string
request.methodHTTP method (GET, POST, etc.)
request.pathNormalized request path
request.remoteIpAddressClient 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 CodeCondition
200Success (or custom code via statusCode output)
400Input validation failed (missing required params, type mismatch)
401Authentication failed (missing or invalid credentials)
404Organization not found or no matching endpoint
413Request body exceeds maxBodySize
429Rate limit exceeded
504Workflow execution timed out
500Unexpected 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:

CodeRule
PAPI_001api section is required
PAPI_002api.path is required
PAPI_003api.method is required
PAPI_004api.method must be GET, POST, PUT, PATCH, or DELETE
PAPI_005api.authentication must be none, bearer, or apiKey (if provided)
PAPI_006executionMode must be Sync
PAPI_007Every {param} in the path must have a matching input with in: path
PAPI_008At most one input can have in: body
PAPI_010Path must start with / and must not contain ..
PAPI_011Outputs must include a response output
PAPI_012Warning: 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:

  1. Method matching: The HTTP method must match exactly (case-insensitive)
  2. Path matching: Path templates like /orders/{orderId}/items/{itemId} are converted to regex patterns with named capture groups
  3. Specificity: Routes with more static segments take priority over parameterized routes. For example, /orders/recent matches before /orders/{orderId}
  4. 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 document value for organized Swagger documentation
  • Set appropriate rate limits to protect your system from abuse
  • Use apiKey authentication for partner integrations where you control key distribution
  • Use none authentication only for truly public endpoints (tracking pages, status checks)
  • Always validate inputs using required fields and type constraints
  • Set a reasonable timeout based on the expected workflow execution time
  • Return meaningful status codes via the statusCode output (201 for creates, 404 for not found, etc.)