#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:
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.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.