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

  1. Define callback payload schemas using [OpenApiSchemaComponent] so they appear under components.schemas.
  2. Declare callback operations with [OpenApiCallback]:

    • Use Expression = '$request.body#/callbackUrls/<name>' to reference a client-provided callback URL.
    • Use HttpVerb + Pattern for the callback request your API will send.
  3. Attach callbacks to the main operation with [OpenApiCallbackRef] on the primary route (here: POST /v1/payments).
  4. Rely on comment-based help (<# .SYNOPSIS/.DESCRIPTION/.PARAMETER #>) to populate OpenAPI summary/description.
  5. Enable callback automation with Add-KrAddCallbacksAutomation if 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