Component Callbacks
Demonstrates how to define reusable callback components for asynchronous operations.
This example models a realistic payment creation flow where the client provides callback URLs, and the API documents the callback requests it may send.
Full source
File: pwsh/tutorial/examples/10.11-OpenAPI-Component-Callback.ps1
<#
Sample: OpenAPI Callback Components
Purpose: Demonstrate OpenAPI callbacks for async event notifications.
File: 10.11-OpenAPI-Component-Callback.ps1
Notes: Shows a realistic payment flow where the client supplies callback URLs
and the provider documents the callback requests it may send.
#>
param(
[int]$Port = 5000,
[IPAddress]$IPAddress = [IPAddress]::Loopback
)
# --- Logging / Server ---
New-KrLogger | Add-KrSinkConsole |
Set-KrLoggerLevel -Value Debug |
Register-KrLogger -Name 'console' -SetAsDefault
New-KrServer -Name 'OpenAPI Hello World'
Add-KrEndpoint -Port $Port -IPAddress $IPAddress
# =========================================================
# TOP-LEVEL OPENAPI
# =========================================================
Add-KrOpenApiInfo -Title 'Hello World API' `
-Version '1.0.0' `
-Description 'Demonstrates OpenAPI callbacks (async notifications) using component references and inline callbacks.'
Add-KrOpenApiContact -Email 'support@example.com'
# Enable Kestrun callback automation middleware
Add-KrAddCallbacksAutomation
# Add Server info
#Add-KrOpenApiServer -Url "http://$IPAddress:$Port" -Description 'Local Server'
# =========================================================
# COMPONENT SCHEMAS
# =========================================================
[OpenApiSchemaComponent(Description = 'Callback URLs supplied by the client for async notifications.', RequiredProperties = ('status', 'reservation', 'shippingOrder'))]
class CallbackUrls {
[OpenApiPropertyAttribute(Description = 'Callback URL for payment status updates.', Format = 'uri', Example = 'https://client.example.com/callbacks/payment-status')]
[string]$status
[OpenApiPropertyAttribute(Description = 'Callback URL for reservation created events.', Format = 'uri', Example = 'https://client.example.com/callbacks/reservation')]
[string]$reservation
[OpenApiPropertyAttribute(Description = 'Callback URL for shipping order created events.', Format = 'uri', Example = 'https://client.example.com/callbacks/shipping-order')]
[string]$shippingOrder
}
[OpenApiSchemaComponent(Description = 'Request payload to create a payment. Includes callback URLs for async updates.', RequiredProperties = ('amount', 'currency', 'callbackUrls'))]
class CreatePaymentRequest {
[OpenApiPropertyAttribute(Description = 'Payment amount', Example = 129.99, Minimum = 0)]
[double]$amount
[OpenApiPropertyAttribute(Description = 'Currency code', Example = 'USD')]
[string]$currency
[OpenApiPropertyAttribute(Description = 'Client-provided callback URLs')]
[CallbackUrls]$callbackUrls
}
[OpenApiSchemaComponent(Description = 'Response returned after creating a payment.', RequiredProperties = ('paymentId', 'status'))]
class CreatePaymentResponse {
[OpenApiPropertyAttribute(Description = 'Payment identifier', Example = 'PAY-12345678')]
[string]$paymentId
[OpenApiPropertyAttribute(Description = 'Current payment status', Example = 'pending')]
[ValidateSet('pending', 'authorized', 'captured', 'failed')]
[string]$status
}
[OpenApiSchemaComponent(Description = 'Callback event payload for payment status changes.', RequiredProperties = ('eventId', 'timestamp', 'paymentId', 'status'))]
class PaymentStatusChangedEvent {
[OpenApiPropertyAttribute(Description = 'Event identifier', Format = 'uuid')]
[string]$eventId
[OpenApiPropertyAttribute(Description = 'When the event occurred', Format = 'date-time')]
[datetime]$timestamp
[OpenApiPropertyAttribute(Description = 'Payment identifier', Example = 'PAY-12345678')]
[string]$paymentId
[OpenApiPropertyAttribute(Description = 'New status', Example = 'authorized')]
[ValidateSet('authorized', 'captured', 'failed')]
[string]$status
}
[OpenApiSchemaComponent(Description = 'Callback event payload for reservation creation.', RequiredProperties = ('eventId', 'timestamp', 'orderId', 'reservationId'))]
class ReservationCreatedEvent {
[OpenApiPropertyAttribute(Description = 'Event identifier', Format = 'uuid')]
[string]$eventId
[OpenApiPropertyAttribute(Description = 'When the event occurred', Format = 'date-time')]
[datetime]$timestamp
[OpenApiPropertyAttribute(Description = 'Order identifier', Example = 'ORD-98765432')]
[string]$orderId
[OpenApiPropertyAttribute(Description = 'Reservation identifier', Example = 'RSV-12345678')]
[string]$reservationId
}
[OpenApiSchemaComponent(Description = 'Callback event payload for shipping order creation.', RequiredProperties = ('eventId', 'timestamp', 'orderId', 'shippingOrderId'))]
class ShippingOrderCreatedEvent {
[OpenApiPropertyAttribute(Description = 'Event identifier', Format = 'uuid')]
[string]$eventId
[OpenApiPropertyAttribute(Description = 'When the event occurred', Format = 'date-time')]
[datetime]$timestamp
[OpenApiPropertyAttribute(Description = 'Order identifier', Example = 'ORD-98765432')]
[string]$orderId
[OpenApiPropertyAttribute(Description = 'Shipping order identifier', Example = 'SHP-12345678')]
[string]$shippingOrderId
}
# =========================================================
# CALLBACKS COMPONENTS
# =========================================================
<#
.SYNOPSIS
Payment Status Callback (component)
.DESCRIPTION
Provider calls the consumer back when a payment status changes.
.PARAMETER paymentId
The ID of the payment
.PARAMETER Body
The callback event payload
#>
function paymentStatusCallback {
[OpenApiCallback(
Expression = '$request.body#/callbackUrls/status',
HttpVerb = 'post',
Pattern = '/v1/payments/{paymentId}/status',
Inline = $true
)]
param(
[OpenApiParameter(In = 'path', Required = $true)]
[string]$paymentId,
[OpenApiRequestBody(ContentType = 'application/json')]
[PaymentStatusChangedEvent]$Body
)
}
<#
.SYNOPSIS
Reservation Callback (component)
.DESCRIPTION
Provider calls the consumer back when a reservation is made.
.PARAMETER orderId
The ID of the order
.PARAMETER Body
The callback event payload
#>
function reservationCallback {
[OpenApiCallback(
Expression = '$request.body#/callbackUrls/reservation',
HttpVerb = 'post',
Pattern = '/v1/orders/{orderId}/reservation'
)]
param(
[OpenApiParameter(In = 'path', Required = $true)]
[string]$orderId,
[OpenApiRequestBody(ContentType = 'application/json')]
[ReservationCreatedEvent]$Body
)
}
<#
.SYNOPSIS
Shipping Order Callback (component)
.DESCRIPTION
Provider calls the consumer back when a shipping order is created.
.PARAMETER orderId
The ID of the order
.PARAMETER Body
The callback event payload
#>
function shippingOrderCallback {
[OpenApiCallback(
Expression = '$request.body#/callbackUrls/shippingOrder',
HttpVerb = 'post',
Pattern = '/v1/orders/{orderId}/shippingOrder'
)]
param(
[OpenApiParameter(In = 'path', Required = $true)]
[string]$orderId,
[OpenApiRequestBody(ContentType = 'application/json')]
[ShippingOrderCreatedEvent]$Body
)
}
# =========================================================
# ENABLE KR CONFIGURATION
# =========================================================
Enable-KrConfiguration
Add-KrApiDocumentationRoute -DocumentType Swagger
Add-KrApiDocumentationRoute -DocumentType Redoc
Add-KrApiDocumentationRoute -DocumentType Scalar
Add-KrApiDocumentationRoute -DocumentType Rapidoc
Add-KrApiDocumentationRoute -DocumentType Elements
# =========================================================
# ROUTES / OPERATIONS
# =========================================================
<#
.SYNOPSIS
Create a payment.
.DESCRIPTION
Creates a new payment and demonstrates how callbacks are documented.
The request includes callback URLs used by the provider to notify the consumer asynchronously.
.PARAMETER body
Payment creation request including callback URLs.
#>
function createPayment {
[OpenApiPath(HttpVerb = 'post', Pattern = '/v1/payments')]
[OpenApiResponse(StatusCode = '201', Description = 'Payment created', Schema = [CreatePaymentResponse], ContentType = 'application/json')]
[OpenApiResponse(StatusCode = '400', Description = 'Invalid input')]
[OpenApiCallbackRef(Key = 'paymentStatus', ReferenceId = 'paymentStatusCallback', Inline = $true)]
[OpenApiCallbackRef(Key = 'reservation', ReferenceId = 'reservationCallback')]
[OpenApiCallbackRef(Key = 'shippingOrder', ReferenceId = 'shippingOrderCallback')]
param(
[OpenApiParameter(In = 'query', Required = $true)]
[string]$orderId = 'ord-12345678',
[OpenApiRequestBody(ContentType = 'application/json')]
[CreatePaymentRequest]$body
)
if (-not $body -or -not $body.amount -or -not $body.currency -or -not $body.callbackUrls) {
Write-KrJsonResponse @{ error = 'amount, currency, and callbackUrls are required' } -StatusCode 400
return
}
$paymentId = 'PAY-' + ([guid]::NewGuid().ToString('N').Substring(0, 8))
Write-Host "PaymentId: $paymentId OrderId: $orderId"
# Create callback events payloads
$shippingOrderEvent = [ShippingOrderCreatedEvent]@{
eventId = [guid]::NewGuid().ToString()
timestamp = (Get-Date).ToUniversalTime()
orderId = $orderId
shippingOrderId = 'SHP-' + ([guid]::NewGuid().ToString('N').Substring(0, 8))
}
# Create callback event payloads
$reservationEvent = [ReservationCreatedEvent]@{
eventId = [guid]::NewGuid().ToString()
timestamp = (Get-Date).ToUniversalTime()
orderId = $orderId
reservationId = 'RSV-' + ([guid]::NewGuid().ToString('N').Substring(0, 8))
}
# Payment callback event payload
$paymentStatusChangedEvent = [PaymentStatusChangedEvent]@{
eventId = [guid]::NewGuid().ToString()
timestamp = (Get-Date).ToUniversalTime()
paymentId = $paymentId
status = 'authorized'
}
Expand-KrObject $shippingOrderEvent -Label 'ShippingOrderCreatedEvent'
Expand-KrObject $reservationEvent -Label 'ReservationCreatedEvent'
Expand-KrObject $paymentStatusChangedEvent -Label 'PaymentStatusChangedEvent'
shippingOrderCallback -OrderId $orderId -Body $shippingOrderEvent
reservationCallback -OrderId $orderId -Body $reservationEvent
paymentStatusCallback -PaymentId $paymentId -Body $paymentStatusChangedEvent
Write-Host "Created payment for Amount: $($body.amount) $($body.currency)"
$paymentId = 'PAY-' + ([guid]::NewGuid().ToString('N').Substring(0, 8))
Write-KrJsonResponse ([ordered]@{
paymentId = $paymentId
status = 'pending'
}) -StatusCode 201
}
<#
.SYNOPSIS
Get payment.
.DESCRIPTION
Simple endpoint to retrieve a payment resource.
.PARAMETER paymentId
The payment identifier.
#>
function getPayment {
[OpenApiPath(HttpVerb = 'get', Pattern = '/v1/payments/{paymentId}')]
[OpenApiResponse(StatusCode = '200', Description = 'Payment found', Schema = [CreatePaymentResponse], ContentType = 'application/json')]
[OpenApiResponse(StatusCode = '404', Description = 'Payment not found')]
param(
[OpenApiParameter(In = 'path', Required = $true)]
[string]$paymentId
)
Write-KrJsonResponse ([ordered]@{
paymentId = $paymentId
status = 'pending'
}) -StatusCode 200
}
<#
.SYNOPSIS
Payment Status Callback Receiver
.DESCRIPTION
Endpoint to receive payment status change callbacks.
.PARAMETER paymentId
The ID of the payment.
.PARAMETER Body
The callback event payload.
#>
function paymentstatus {
[OpenApiPath(HttpVerb = 'post', Pattern = '/callbacks/payment-status/v1/payments/{paymentId}/status')]
[OpenApiResponse(StatusCode = '204', Description = 'Accepted')]
param(
[OpenApiParameter(In = 'path', Required = $true)]
[string]$paymentId,
[OpenApiRequestBody(ContentType = 'application/json')]
[PaymentStatusChangedEvent]$Body
)
Write-Host "Received payment status callback for PaymentId: $paymentId, Status: $($Body.status)"
Expand-KrObject $Body
Write-KrStatusResponse -StatusCode 204
}
<#
.SYNOPSIS
Reservation Callback Receiver
.DESCRIPTION
Endpoint to receive reservation callbacks.
.PARAMETER orderId
The ID of the order.
.PARAMETER Body
The callback event payload.
#>
function reservation {
[OpenApiPath(HttpVerb = 'post', Pattern = '/callbacks/reservation/v1/orders/{orderId}/reservation')]
[OpenApiResponse(StatusCode = '204', Description = 'Accepted')]
param(
[OpenApiParameter(In = 'path', Required = $true)]
[string]$orderId,
[OpenApiRequestBody(ContentType = 'application/json')]
[ReservationCreatedEvent]$Body
)
Write-Host "Received reservation callback for OrderId: $orderId, Status: $($Body.status)"
Expand-KrObject $Body
Write-KrStatusResponse -StatusCode 204
}
<#
.SYNOPSIS
Shipping Order Callback Receiver
.DESCRIPTION
Endpoint to receive shipping order callbacks.
.PARAMETER orderId
The ID of the order.
.PARAMETER Body
The callback event payload.
#>
function shippingorder {
[OpenApiPath(HttpVerb = 'post', Pattern = '/callbacks/shipping-order/v1/orders/{orderId}/shippingOrder')]
[OpenApiResponse(StatusCode = '204', Description = 'Accepted')]
param(
[OpenApiParameter(In = 'path', Required = $true)]
[string]$orderId,
[OpenApiRequestBody(ContentType = 'application/json')]
[ShippingOrderCreatedEvent]$Body
)
Write-Host "Received shipping order callback for OrderId: $orderId, Status: $($Body.status)"
Expand-KrObject $Body
Write-KrStatusResponse -StatusCode 204
}
# =========================================================
# OPENAPI DOC ROUTE / BUILD
# =========================================================
Add-KrOpenApiRoute # Default pattern '/openapi/{version}/openapi.{format}'
Build-KrOpenApiDocument
Test-KrOpenApiDocument
# =========================================================
# RUN SERVER
# =========================================================
Start-KrServer -CloseLogsOnExit
Step-by-step
- Define callback payload schemas using
[OpenApiSchemaComponent]so they appear undercomponents.schemas. -
Declare callback operations with
[OpenApiCallback]:- Use
Expression = '$request.body#/callbackUrls/<name>'to reference a client-provided callback URL. - Use
HttpVerb+Patternfor the callback request your API will send.
- Use
- Attach callbacks to the main operation with
[OpenApiCallbackRef]on the primary route (here:POST /v1/payments). - Rely on comment-based help (
<# .SYNOPSIS/.DESCRIPTION/.PARAMETER #>) to populate OpenAPIsummary/description. - Enable callback automation with
Add-KrAddCallbacksAutomationif you want those callback functions to actually dispatch HTTP callback requests at runtime (not just appear in OpenAPI).
Try it
Run the example script and inspect the generated OpenAPI:
# From the repo root
Import-Module ./src/PowerShell/Kestrun/Kestrun.psd1 -Force
./docs/_includes/examples/pwsh/10.11-OpenAPI-Component-Callback.ps1 -Port 5000
Then visit:
- OpenAPI JSON:
http://127.0.0.1:5000/openapi/v3.1/openapi.json - Swagger UI:
http://127.0.0.1:5000/swagger - ReDoc:
http://127.0.0.1:5000/redoc
Try the main operation:
$body = @{
amount = 129.99
currency = 'USD'
callbackUrls = @{
# For this tutorial, point callbacks back at the same local server.
# The callback middleware will append the callback pattern (e.g. /v1/payments/{paymentId}/status)
# to these base URLs.
status = 'http://127.0.0.1:5000/callbacks/payment-status'
reservation = 'http://127.0.0.1:5000/callbacks/reservation'
shippingOrder = 'http://127.0.0.1:5000/callbacks/shipping-order'
}
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri 'http://127.0.0.1:5000/v1/payments' -Method Post -ContentType 'application/json' -Body $body
In the OpenAPI document, callbacks appear under:
paths['/v1/payments'].post.callbacks
Referenced callback components appear under:
components.callbacks
Troubleshooting
summary / description are missing in OpenAPI
- Ensure you used standard comment-based help blocks:
<# ... #>(not<#+ ... #>).
Callback schemas are missing from components.schemas
- Ensure the PowerShell classes are decorated with
[OpenApiSchemaComponent].
Request body is not bound (400 / missing properties)
- Ensure
[OpenApiRequestBody(...)]is applied to the body parameter of the function.
References
Previous / Next
Previous: Component Links Next: WebHooks