Skip to content
Connected Services Framework
Part 2 — Implementation
Markdown

#Appendix: OpenAPI Specification

Connected Services Framework (CSF) — Part 2: Implementation — Version 2.0

#Overview

This appendix contains the OpenAPI 3.0.3 specification for the CSF Letterbox API. It defines the message sending/receiving interface between MAPs, including:

  • The JAM envelope structure (source, destination, routingID, auditData)
  • CSF-specific HTTP headers for DKIM signing (X-CSF-SIGNATURE, X-CSF-SIGNATURE-DATESTAMP) and route tracking (X_CSF_ROUTE)
  • OAuth 2.0 Bearer authentication
  • All synchronous error responses (HTTP 4xx/5xx with 9xxx and 8xxx error codes)
  • The asynchronous message delivery failure structure

This specification is compatible with the TOTSCo Hub API Specification v2.0. MAPs implementing this spec can exchange messages with both CSF MAPs and the TOTSCo Hub.

This OpenAPI specification should be reviewed alongside the Message API documentation to ensure all error codes, field descriptions, and examples are aligned. The messageBody is modelled as a generic object — in production, the property name should match the routingID (e.g., businessSwitchMatchRequest).


#Specification

openapi: 3.0.3
info:
  title: CSF Letterbox API
  version: "2.0.0"
  description: |
    The Connected Services Framework (CSF) Letterbox API enables Managed Access
    Providers (MAPs) to exchange messages on behalf of their Communications
    Providers (CPs) over a secure, peer-to-peer transport layer.

    This API is compatible with the TOTSCo Hub letterbox interface (v2.0) and
    uses the industry-standard JSON Asynchronous Messaging (JAM) envelope for
    addressing, routing, and correlation.

    ## Key Characteristics

    - **Transport**: HTTPS over TLS 1.3 (mandatory)
    - **Authentication**: OAuth 2.0 Bearer Token (Client Credentials flow)
    - **Message Signing**: DKIM-based PKI via `X-CSF-SIGNATURE` header (RFC 6376)
    - **Acceptance Model**: HTTP 202  accepted for processing, not yet completed
    - **Error Model**: Synchronous 4xx/5xx with 9xxx (industry) and 8xxx (CSF) codes

    ## Message Structure

    Every message consists of:
    1. An **envelope** containing delivery instructions (source, destination,
       routingID, auditData)  defined by the JAM Specification
    2. A **message body** containing the industry process payload (e.g., SforB
       Switch Match)  defined by the relevant process specification

    The actual message body property name MUST match the `routingID`
    (e.g., `businessSwitchMatchRequest`). This specification models it as a
    generic `messageBody` object for portability.

  contact:
    name: Telecom Technical Architecture Group (TAG)
  license:
    name: Open  Free for any CP or MAP to use
    url: https://github.com/nowyoyo/csf-documentation

servers:
  - url: https://{fqdn}/letterbox
    description: MAP Letterbox Endpoint
    variables:
      fqdn:
        default: api.example-map.com
        description: The MAP's Fully Qualified Domain Name (RFC 1035)

paths:
  /{version}/post:
    post:
      operationId: postMessage
      summary: Post a JAM message to the letterbox
      description: |
        Accepts a JAM-formatted JSON message and returns **202 Accepted** if
        the message passes validation and is accepted for processing.

        ### Validation Sequence

        The receiving MAP validates the message in the following order:

        1. OAuth 2.0 token validity
        2. JSON message structure
        3. Envelope attribute validation
        4. Source type and RCPID validity
        5. Source RCPID account status
        6. Destination type and RCPID validity
        7. Destination RCPID account status
        8. Source MAP authorisation via `_mapkey` DNS check (HTTP 403 + 9301 on mismatch)  see Message API §4.4.1
        9. Routing ID validity
        10. DKIM signature verification (via `X-CSF-SIGNATURE`)

        Validation stops at the first failure and the corresponding error is returned.

        ### DKIM Signing (CSF MAPs only)

        Messages between CSF MAPs MUST include `X-CSF-SIGNATURE` and
        `X-CSF-SIGNATURE-DATESTAMP` headers. Messages from HMAPs (e.g., TOTSCo)
        are not required to include these headers.

      tags:
        - Messaging
      security:
        - bearerAuth: []

      parameters:
        - name: version
          in: path
          required: true
          description: |
            Letterbox API version number. This version will only change if there
            is a substantial update in how messages are processed. Previous
            versions remain in service for backward compatibility.
          schema:
            type: string
            pattern: "^[0-9]+\\.[0-9]+$"
            example: "2.0"

        - name: X-CSF-SIGNATURE
          in: header
          required: false
          description: |
            DKIM signature for the message. Required for CSF MAP-to-MAP exchanges.
            Not required for HMAP (e.g., TOTSCo) exchanges.

            Format follows RFC 6376 with CSF-specific tags:
            `a=rsa-sha256; c=simple/simple; s={RCPID}; d={domain}; bh={bodyHash}; h=X-CSF-SIGNATURE-DATESTAMP; b={signature}`

            See PKI & DKIM Signing documentation for full details.
          schema:
            type: string
          example: "a=rsa-sha256; c=simple/simple; s=809b6e65-a6e7-40f6-8b52-04dd65b6fce1; d=gplb-test.nowyoyo.net; bh=/CWGLVBT2gdyO5cudfAEUSF43KHLmlJjU/Nr2YNqkos=; h=X-CSF-SIGNATURE-DATESTAMP; b=4J4OGZfxi..."

        - name: X-CSF-SIGNATURE-DATESTAMP
          in: header
          required: false
          description: |
            Timestamp of when the DKIM signature was created. Required whenever
            `X-CSF-SIGNATURE` is present. Embedded in the signature to protect
            against replay attacks.
          schema:
            type: string
            pattern: "^[0-9]{15}$"
            description: Format yyyyMMddHHmmssS (S = tenth of a second)
          example: "202508121340391"

        - name: X_CSF_ROUTE
          in: header
          required: false
          description: |
            Optional but recommended. Records the route the message has taken.
            Each MAP adds a new header entry (do not append to existing headers).
            Format: `{EpochMilliseconds} {MAPName}` (alphanumerics only).
          schema:
            type: string
          example: "1741947045684 TheYellowMap"

      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LetterboxMessage'
            examples:
              sfbSwitchMatch:
                summary: SforB Switch Match Request
                description: A business Switch Match request using UUIDv4 RCPIDs
                value:
                  envelope:
                    source:
                      type: RCPID
                      identity: "d8322d80-92c8-4906-a408-f1d2daf7e03d"
                      correlationID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    destination:
                      type: RCPID
                      identity: "7d87c2a7-4f64-433c-8ae1-9460f19a0a13"
                    routingID: businessSwitchMatchRequest
                    auditData:
                      - name: "channel"
                        value: "api"
                      - name: "testFlag"
                        value: "false"
                  businessSwitchMatchRequest:
                    customerPostcode: "AB12 3CD"
                    customerSurname: "SMITH"
                    businessName: "Smith Trading Ltd"

              deliveryFailure:
                summary: Message Delivery Failure Notification
                description: Asynchronous notification when a message cannot be delivered
                value:
                  envelope:
                    source:
                      type: RCPID
                      identity: "MAP-X-IDENTITY"
                    destination:
                      type: RCPID
                      identity: "d8322d80-92c8-4906-a408-f1d2daf7e03d"
                      correlationID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    routingID: messageDeliveryFailure
                    auditData:
                      - name: "originalDestinationType"
                        value: "RCPID"
                      - name: "originalDestination"
                        value: "7d87c2a7-4f64-433c-8ae1-9460f19a0a13"
                      - name: "originalRoutingID"
                        value: "businessSwitchMatchRequest"
                      - name: "faultCode"
                        value: "9008"
                  messageDeliveryFailure:
                    code: "9008"
                    text: "Unable to deliver the message to the destination, timed out."
                    severity: "failure"

      responses:
        '202':
          description: |
            **Accepted** — The message has been accepted for processing, but
            processing has not been completed. The request might or might not
            be eventually acted upon and may be disallowed when processing occurs.

            No JSON body is returned on success.

        '400':
          description: |
            **Bad Request** — The message failed validation. This covers:
            - Invalid JSON structure
            - Missing required envelope fields
            - Invalid source/destination type or RCPID
            - Invalid routing ID
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ValidationError'
                  - $ref: '#/components/schemas/HttpError'
              examples:
                invalidDestinationType:
                  summary: Unknown destination type (9000)
                  value:
                    errorCode: "9000"
                    errorText: "Unknown or missing destination Type."
                invalidDestinationId:
                  summary: Unknown destination RCPID (9001)
                  value:
                    errorCode: "9001"
                    errorText: "Unknown or Invalid destination."
                invalidSourceType:
                  summary: Unknown source type (9002)
                  value:
                    errorCode: "9002"
                    errorText: "Unknown or invalid source Type."
                invalidSourceId:
                  summary: Unknown source RCPID (9003)
                  value:
                    errorCode: "9003"
                    errorText: "Unknown or invalid source Id."
                invalidRoutingMapping:
                  summary: No routing ID mapped to source (9010)
                  value:
                    errorCode: "9010"
                    errorText: "No routingID is mapped with Source RCP."
                invalidRoutingId:
                  summary: Unknown routing ID (9012)
                  value:
                    errorCode: "9012"
                    errorText: "Unknown or invalid routing ID."
                schemaValidation:
                  summary: Schema validation failure
                  value:
                    code: "400"
                    message: "Bad Request"
                    description: "Schema validation failed in the Request: [Path '/directory'] Object has missing required properties ([\"listID\"])"

        '401':
          description: |
            **Unauthorised** — Authentication failed. This covers:
            - Invalid or expired OAuth 2.0 bearer token
            - Source type and ID not permitted from originating location (9004)
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ValidationError'
                  - $ref: '#/components/schemas/HttpError'
              examples:
                sourceNotPermitted:
                  summary: Source not permitted from this location (9004)
                  value:
                    errorCode: "9004"
                    errorText: "Source type and ID not permitted from originating location."
                invalidCredentials:
                  summary: Invalid OAuth 2.0 credentials
                  value:
                    code: "900901"
                    message: "Invalid Credentials"
                    description: "Access failure for API: /letterbox/v2.0, version: v2.0 status: (900901) - Invalid Credentials. Make sure you have provided the correct security credentials."

        '403':
          description: |
            **Forbidden** — Authorisation succeeded but the request is not
            permitted. This covers:
            - Source RCPID account status is not valid (9003)
            - Destination RCPID account status is not valid (9001)
            - DKIM signature verification failed (8101 PERM_FAIL, 8102 TEMP_FAIL)
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ValidationError'
                  - $ref: '#/components/schemas/PkiError'
              examples:
                sourceAccountInvalid:
                  summary: Source RCPID account status invalid (9003)
                  value:
                    errorCode: "9003"
                    errorText: "Source RCPD ID account status is not valid."
                destinationAccountInvalid:
                  summary: Destination RCPID account status invalid (9001)
                  value:
                    errorCode: "9001"
                    errorText: "Destination RCPD ID account status is not valid."
                pkiPermFail:
                  summary: DKIM signature permanently failed (8101)
                  value:
                    errorCode: 8101
                    errorText: "Domain gplb-test.nowyoyo.net is not valid key source for CP 809b6e65-a6e7-40f6-8b52-04dd65b6fce1"
                pkiTempFail:
                  summary: DKIM signature temporarily failed (8102)
                  value:
                    errorCode: 8102
                    errorText: "Unable to resolve 809b6e65-a6e7-40f6-8b52-04dd65b6fce1._domainkey.gplb-test.nowyoyo.net. TXT entry does not exist."
                pkiBodyHashMismatch:
                  summary: Body hash mismatch (8101)
                  value:
                    errorCode: 8101
                    errorText: "Body Hash 3hzlA9zA8SAVapmd5ZZnMdwaZpk/WOyQXixrqVu/WMc= is different to signature /CWGLVBT2gdyO5cudfAEUSF43KHLmlJjU/Nr2YNqkos="
                pkiMissingTags:
                  summary: Missing mandatory signature tags (8101)
                  value:
                    errorCode: 8101
                    errorText: "Signature has missing mandatory tag(s): [b, bh]"

        '404':
          description: |
            **Not Found** — The requested resource was not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HttpError'
              examples:
                notFound:
                  value:
                    code: "404"
                    message: "Runtime Error"
                    description: "No matching resource found for given API Request"

        '405':
          description: |
            **Method Not Allowed** — The HTTP method is not supported
            (e.g., GET instead of POST).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HttpError'
              examples:
                methodNotAllowed:
                  value:
                    code: "405"
                    message: "Runtime Error"
                    description: "Method not allowed for given API resource"

        '429':
          description: |
            **Too Many Requests** — Rate limit exceeded. The `nextAccessTime`
            field indicates when the client can retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThrottledError'
              examples:
                throttled:
                  value:
                    code: "900804"
                    message: "Message throttled out"
                    description: "MAP exceeded the quota. You can access API after 2026-Apr-26 04:43:00+0000 UTC"
                    nextAccessTime: "2026-Apr-26 04:43:00+0000 UTC"

        '500':
          description: |
            **Internal Server Error** — An unexpected error occurred during processing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HttpError'
              examples:
                internalError:
                  value:
                    code: "101503"
                    message: "Runtime Error"
                    description: "Error connecting to the back end"

        '501':
          description: |
            **Not Implemented** — The HTTP method is not supported by the server.

        '502':
          description: |
            **Bad Gateway** — Invalid response from an upstream server.

        '503':
          description: |
            **Service Unavailable** — The server is temporarily unable to handle
            the request (e.g., maintenance). Clients SHOULD respect the
            `Retry-After` header if present.

        '504':
          description: |
            **Gateway Timeout** — The server did not receive a timely response
            from an upstream server.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        OAuth 2.0 Bearer Token obtained via the Client Credentials flow.
        Request a token from the target MAP's OAuth token endpoint using
        the `client_id` and `client_secret` exchanged during onboarding.

  schemas:

    # ──────────────────────────────────────────────
    # Request Body
    # ──────────────────────────────────────────────

    LetterboxMessage:
      type: object
      required:
        - envelope
      properties:
        envelope:
          $ref: '#/components/schemas/Envelope'
      additionalProperties:
        type: object
        description: |
          The message body. The property name MUST match the `routingID`
          in the envelope (e.g., `businessSwitchMatchRequest`,
          `messageDeliveryFailure`). Modelled here as `additionalProperties`
          because the key is dynamic.
      description: |
        A JAM-formatted message consisting of an envelope and a
        process-specific message body.

    Envelope:
      type: object
      required:
        - source
        - destination
        - routingID
      properties:
        source:
          $ref: '#/components/schemas/EndpointRef'
          description: |
            The originator of the message and return address for any
            response. The `correlationID` MUST always be provided in source.
        destination:
          $ref: '#/components/schemas/EndpointRef'
          description: |
            The intended recipient. The `correlationID` is only present in
            response messages — it contains the original sender's correlationID.
        routingID:
          type: string
          description: |
            Determines the message type and routing. Must be a value supported
            by the recipient MAP's routing groups. Valid values include
            `businessSwitchMatchRequest`, `businessSwitchOrderRequest`,
            `residentialSwitchMatchRequest`, `messageDeliveryFailure`,
            and `notification`.
          example: businessSwitchMatchRequest
        auditData:
          type: array
          description: |
            Name-value pairs for regulatory reporting, diagnostics, and
            industry auditing. Each industry process defines its own audit
            requirements. The `testFlag` audit field MUST be set to `"true"`
            for all test traffic.
          items:
            $ref: '#/components/schemas/AuditDatum'

    EndpointRef:
      type: object
      required:
        - type
        - identity
      properties:
        type:
          type: string
          description: |
            The directory list type. Currently only `RCPID` is supported.
          enum:
            - RCPID
          example: RCPID
        identity:
          type: string
          description: |
            The RCPID of the sender (source) or recipient (destination).
            For CSF, this is a UUIDv4 (36 characters). For TOTSCo
            compatibility, 4-character OTS codes are also accepted.
          example: "d8322d80-92c8-4906-a408-f1d2daf7e03d"
        correlationID:
          type: string
          description: |
            In source: MUST always be provided. Should be sufficiently unique
            to allow correlation of responses over a reasonable period. UUIDs
            are recommended.

            In destination: only present in response messages, containing the
            original sender's correlationID.
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

    AuditDatum:
      type: object
      required:
        - name
        - value
      properties:
        name:
          type: string
          description: The audit field name
          example: "channel"
        value:
          type: string
          description: The audit field value
          example: "api"

    # ──────────────────────────────────────────────
    # Asynchronous Delivery Failure Body
    # ──────────────────────────────────────────────

    MessageDeliveryFailure:
      type: object
      required:
        - code
        - text
        - severity
      properties:
        code:
          type: string
          description: |
            Numeric fault code for remedial action. Uses 9xxx series codes.
          example: "9008"
        text:
          type: string
          description: Human-readable description of the fault
          example: "Unable to deliver the message to the destination, timed out."
        severity:
          type: string
          description: |
            Nature of the notification.
          enum:
            - information
            - warning
            - failure
          example: failure
      description: |
        Sent asynchronously by a MAP when it cannot deliver a message to the
        intended recipient. The envelope source is the MAP that failed to
        deliver, and the envelope destination contains the original sender's
        correlationID.

        ### Delivery Failure Codes (9xxx)

        | Code | Description | Severity |
        |------|-------------|----------|
        | 9005 | No valid route to destination | failure |
        | 9006 | Rejected — invalid message format (HTTP 400) | failure |
        | 9007 | Recipient rejected message (HTTP 404) | failure |
        | 9008 | Timed out after retries (HTTP 5xx or timeout) | failure |
        | 9013 | Invalid API key | failure |
        | 9014 | API key expired | failure |
        | 9015 | Digital certificate invalid | failure |
        | 9016 | Digital certificate expired | failure |

    # ──────────────────────────────────────────────
    # Error Response Bodies
    # ──────────────────────────────────────────────

    ValidationError:
      type: object
      required:
        - errorCode
        - errorText
      properties:
        errorCode:
          type: integer
          description: |
            Numeric error code from the 9xxx (industry) or 8xxx (CSF) series.
          example: "9002"
        errorText:
          type: string
          description: |
            Human-readable description of the error with suggested
            remedial action.
          example: "Unknown or invalid source Type."
      description: |
        Returned for validation errors detected during envelope processing.
        Uses 9xxx codes for industry-standard validations and 8xxx codes
        for CSF PKI errors.

        ### Validation Error Codes (9xxx)

        | Code | HTTP | Description |
        |------|------|-------------|
        | 9000 | 400 | Unknown or missing destination type |
        | 9001 | 400 | Unknown or invalid destination RCPID |
        | 9001 | 403 | Destination RCPID account status invalid |
        | 9002 | 400 | Unknown or invalid source type |
        | 9003 | 400 | Unknown or invalid source RCPID |
        | 9003 | 403 | Source RCPID account status invalid |
        | 9004 | 401 | Source not permitted from originating location |
        | 9010 | 400 | No routing ID mapped to source RCP |
        | 9012 | 400 | Unknown or invalid routing ID |

        ### PKI Error Codes (8xxx)

        | Code | HTTP | Description | Retry? |
        |------|------|-------------|--------|
        | 8101 | 403 | PERM_FAIL — signature verification permanently failed | No |
        | 8102 | 403 | TEMP_FAIL — signature verification temporarily failed | Yes |

    PkiError:
      type: object
      required:
        - errorCode
        - errorText
      properties:
        errorCode:
          type: integer
          enum:
            - 8101
            - 8102
          description: |
            8101 = PERM_FAIL (do not retry)
            8102 = TEMP_FAIL (retry with backoff)
        errorText:
          type: string
          description: Detailed description of the PKI failure
      description: |
        Returned with HTTP 403 when DKIM signature verification fails.
        Only used for CSF MAP-to-MAP exchanges (not sent to TOTSCo).

    HttpError:
      type: object
      properties:
        code:
          type: string
          description: HTTP status code or internal process code
          example: "400"
        message:
          type: string
          description: Short error classification
          example: "Bad Request"
        description:
          type: string
          description: Detailed error description with remedial guidance
          example: "Schema validation failed in the Request"
      description: |
        Generic HTTP error response. Used for schema validation failures,
        authentication errors, and server-side errors.

    ThrottledError:
      type: object
      properties:
        code:
          type: string
          description: Internal process code (not an HTTP error code)
          example: "900804"
        message:
          type: string
          example: "Message throttled out"
        description:
          type: string
          example: "MAP exceeded the quota."
        nextAccessTime:
          type: string
          description: |
            When the client can retry. Format varies by implementation.
          example: "2026-Apr-26 04:43:00+0000 UTC"
      description: |
        Returned with HTTP 429 when the receiving MAP's rate limit is exceeded.

#Usage Notes

#Content-Type

All requests MUST use Content-Type: application/json with charset UTF-8.

#TLS Requirement

All connections MUST use TLS 1.3. Earlier TLS versions MUST be rejected.

#DKIM Headers

For CSF MAP-to-MAP exchanges, the X-CSF-SIGNATURE and X-CSF-SIGNATURE-DATESTAMP headers are mandatory. For messages from HMAPs (e.g., TOTSCo), these headers are optional.

When a MAP forwards a message (e.g., TOTSCo passing a CSF-signed message to its CPs), the X-CSF-SIGNATURE header SHOULD be left intact for end-to-end verification.

#Route Tracking

The X_CSF_ROUTE header is optional but recommended. Each MAP that handles the message adds a new header (not appended to existing headers). The final recipient can read all X_CSF_ROUTE headers to reconstruct the message route.

#Dynamic Message Body Key

The JAM specification allows the message body property name to match the routingID. For example, a message with routingID: "businessSwitchMatchRequest" would have its body under:

{
  "envelope": { ... },
  "businessSwitchMatchRequest": { ... }
}

The OpenAPI specification models this using additionalProperties on the root LetterboxMessage object, since the key name is dynamic.

#Error Response Formats

The CSF uses two distinct error response formats, each governed by an industry standard that the framework cannot modify. This is a statement of fact, not a review item:

  1. ValidationError (errorCode / errorText) — for 9xxx and 8xxx validation and PKI errors. This format is defined by the industry-standard JSON Asynchronous Messaging (JAM) Specification and is used consistently across CSF and TOTSCo implementations. Changing it would break compatibility with the wider switching ecosystem.
  2. HttpError (code / message / description) — for HTTP-level and schema errors. This format follows established IETF RFC standards for HTTP error responses and cannot be modified without breaking standards compliance.

Implementers MUST support receiving both formats. The dual-format design reflects the layered nature of the CSF (industry application protocol over HTTP transport) — it is not transitional and is not under review.