Written by Technical Team | Last updated 27.09.2025 | 14 minute read
If you work with NHS integrations, you’ll encounter the Message Exchange for Social Care and Health (MESH) sooner rather than later. MESH is the national, secure “post-box” for moving clinical and administrative files between organisations. It’s robust, built for very large payloads, and, crucially, it’s designed to be consumed by software rather than people. The linchpin of a successful MESH integration is getting the Authorization header right. Get that wrong and every subsequent call—sending a file, polling an inbox, acknowledging a delivery—will fail.
This article walks through the MESH Authorization header in depth and then moves on to language-agnostic pseudocode you can adapt to your stack. It’s written for engineers who want to go beyond copying and pasting snippets to actually understand what’s going on, why it’s designed that way, and how to implement it cleanly and safely in production.
At its heart, the MESH API uses a custom HMAC-based scheme carried in the standard HTTP Authorization header. You’ll see it advertised as the NHSMESH scheme (note the trailing space in the scheme token), and it’s used in combination with mutual TLS to identify your mailbox and protect requests against tampering and replay. Put simply: the TLS client certificate authenticates the connection; the Authorization header authenticates the request and binds it to your mailbox identity and the current moment in time.
Why both mTLS and an HMAC header? In NHS infrastructure, mTLS ensures you are an approved client presenting a valid certificate issued for the appropriate environment (INT, PTL, Live). But certificates alone don’t give you request-level integrity or strong replay protection. The HMAC signature solves that, ensuring each request is provably yours, tied to your mailbox credentials, and unique in time.
The design borrows from patterns you’ll recognise in other HMAC-signed APIs: a shared secret, a canonical string built from predictable elements, and a cryptographic hash that the server re-computes to verify your claim. The novelty for MESH is in the exact fields and their order, which you must mirror precisely to generate a valid signature. Small deviations—extra separators, wrong timestamp granularity, omitted fields—will invalidate the signature.
The Authorization header value for MESH uses the custom scheme token NHSMESH followed by five colon-separated elements. When rendered, it looks like this:
Authorization: NHSMESH <mailbox_id>:<nonce>:<nonce_count>:<timestamp>:<signature>
Each element has a specific role and validation rule. In broad strokes:
Under the bonnet, the canonical message you sign is:
<mailbox_id>:<nonce>:<nonce_count>:<password>:<timestamp>
…and the HMAC-SHA256 key is your shared secret for that mailbox in the current environment. The password is your mailbox password (distinct from the shared key). That’s an unusual but deliberate choice: the message binds both mailbox identity and your credential at the time of signing, and the server verifies the combination using the registered shared key. The end result is that a valid signature proves you knew the shared key and the password at the time the request was made.
A couple of non-obvious but important points about uniqueness and timing:
This section gives you language-agnostic pseudocode for generating the header consistently across stacks. You can translate it into C#, Java, Python, Node.js, Go or Rust with minimal friction.
function build_mesh_authorization(mailbox_id, mailbox_password, shared_key):
nonce = uuid_v4() // e.g., “6f732ccc-8c6f-4c9b-a22d-2f42978550ad”
nonce_count = 0 // increment when retrying an identical request
timestamp = format(now(), “yyyyMMddHHmm”) // minute precision
message = mailbox_id + “:” + nonce + “:” + str(nonce_count) + “:” + mailbox_password + “:” + timestamp
signature = hex( HMAC_SHA256(shared_key, message) ).lower()
return “NHSMESH ” + mailbox_id + “:” + nonce + “:” + str(nonce_count) + “:” + timestamp + “:” + signature
If you need to retry the exact same HTTP request (same method, URL and body) due to a transient failure, keep the nonce but increment nonce_count before re-signing:
function retry_header(original_nonce, nonce_count, mailbox_id, mailbox_password, shared_key):
new_count = nonce_count + 1
timestamp = format(now(), “yyyyMMddHHmm”)
message = mailbox_id + “:” + original_nonce + “:” + str(new_count) + “:” + mailbox_password + “:” + timestamp
signature = hex(HMAC_SHA256(shared_key, message)).lower()
return “NHSMESH ” + mailbox_id + “:” + original_nonce + “:” + str(new_count) + “:” + timestamp + “:” + signature
With a valid header generator in hand, the rest of your integration becomes straightforward. The MESH API is deliberately simple: send a message to a recipient mailbox (or use workflow-based routing), poll your inbox to find new messages, download each one, then acknowledge to delete it from your inbox. The service acts like a secure, reliable, store-and-forward post-box.
Sending a message
At send time you post to your outbox endpoint and include a few MESH metadata headers alongside your Authorization header. Different workflows will expect different header combinations, but the common ones you’ll see are:
Some national services also publish workflow-specific guidance describing the exact header set they expect. The names are case-insensitive on the wire, but it’s wise to stick to their published casing.
function mesh_send_file(base_url, mailbox_id, mailbox_password, shared_key, file_bytes, headers):
// base_url sample: “https://msg.int.spine2.ncrs.nhs.uk/messageexchange”
auth_header = build_mesh_authorization(mailbox_id, mailbox_password, shared_key)
url = base_url + “/messageexchange/” + url_encode(mailbox_id) + “/outbox”
http_headers = {
“Authorization”: auth_header,
“Content-Type”: “application/octet-stream”,
“Mex-From”: mailbox_id,
“Mex-To”: headers[“to”],
“Mex-Subject”: headers[“subject”],
“Mex-Localid”: headers[“local_id”],
“Mex-WorkflowID”:headers[“workflow_id”]
}
response = HTTP.POST(url, headers=http_headers, body=file_bytes)
assert response.status in [200, 201, 202]
return response.json() or response.headers
For very large files, you’ll typically switch to chunked upload. The flow initiates a message, uploads sequential chunks, and then finalises. The pattern is analogous to multi-part upload in cloud storage: you repeat the same signing logic per HTTP request, but the body and content headers change to carry the chunk number and size. (Exact chunking parameters are subject to the current API specification; the core signing approach remains the same.)
If you don’t know the recipient mailbox ID, you can use the Endpoint Lookup Service with an Organisation Data Service (ODS) code and a workflow to discover the proper recipient mailbox for your use case, then address your send accordingly.
The inbox model is predictable: list the messages, pick one, download the payload, and then acknowledge. You can poll at a sensible interval (for example, once a minute or tied to a queue consumer) and scale out consumers if your volume is high.
function mesh_list_inbox(base_url, mailbox_id, mailbox_password, shared_key):
auth_header = build_mesh_authorization(mailbox_id, mailbox_password, shared_key)
url = base_url + “/messageexchange/” + url_encode(mailbox_id) + “/inbox”
headers = { “Authorization”: auth_header, “Accept”: “application/json” }
response = HTTP.GET(url, headers=headers)
assert response.status == 200
return response.json()[“messages”]
function mesh_download_message(base_url, mailbox_id, mailbox_password, shared_key, message_id):
auth_header = build_mesh_authorization(mailbox_id, mailbox_password, shared_key)
url = base_url + “/messageexchange/” + url_encode(mailbox_id) + “/inbox/” + url_encode(message_id)
headers = { “Authorization”: auth_header, “Accept”: “application/octet-stream” }
response = HTTP.GET(url, headers=headers)
assert response.status == 200
return {
“payload”: response.body_bytes,
“headers”: response.headers
}
When you receive a message, the HTTP response also includes the MESH metadata headers that the sender supplied (subject, workflow ID, local ID, and so on). You’ll typically use these to route internally: for example, workflow ID → handler, local ID → idempotency key, subject → audit log.
Once you’ve durably stored and processed the file, acknowledge the message. MESH deletes it from your inbox when the ACK succeeds, preventing re-processing. You should treat this like a message queue: only ACK after your processing is complete and committed.
function mesh_acknowledge(base_url, mailbox_id, mailbox_password, shared_key, message_id):
auth_header = build_mesh_authorization(mailbox_id, mailbox_password, shared_key)
url = base_url + “/messageexchange/” + url_encode(mailbox_id) + “/inbox/” + url_encode(message_id) + “/acknowledge”
headers = { “Authorization”: auth_header }
response = HTTP.POST(url, headers=headers)
assert response.status in [200, 204]
After sending, you can query for delivery status—whether the message has been collected by the recipient, or if a non-delivery report (NDR) has been raised because the recipient didn’t collect within the retention period. This lets you build clear operational views (“awaiting pickup”, “collected”, “expired/failed”) and trigger follow-ups if needed. The high-level pattern is identical: construct a fresh Authorization header, call the status endpoint, and interpret the result.
Even with a perfect algorithm, reality bites: clocks drift, proxies mangle headers, and staging secrets leak into local configs. This final section collects hard-won practices to help your MESH integration behave like a good citizen in NHS environments.
Use a local or online header validator that can generate or verify the NHSMESH header offline. It’s handy when debugging a failing call: compute the expected signature locally, compare it to what your client produced, and look for differences in the canonical string or key choice. If they deviate, fix your assembly code before touching transport.
Before you try INT or PTL, bring up a sandbox that emulates the MESH API. You can exercise send/poll/ack flows, inspect headers, and simulate large-file chunking without touching national infrastructure. When you graduate to INT, request a test mailbox, shared key and certificate for that environment and confirm everything under mTLS.
Because the timestamp granularity is minutes, the safest pattern is: read the clock, assemble the canonical message, sign, then dispatch immediately. Don’t pre-sign batches or carry pre-signed requests across process boundaries—they’ll age out and fail. Run NTP on every host (or your container nodes) and log both client and server times when diagnosing failures.
Treat the shared key and mailbox password as distinct credentials in your secret store. Rotate them independently. Remember that the shared key is the HMAC key; the password goes inside the canonical message. Don’t log either in plaintext—mask both in application logs and structured traces. (If you ever must log the canonical string during deep debugging, ensure your redaction logic removes the password segment before emitting the line.)
MESH isn’t a pub/sub stream; it’s a mailbox. Poll at a steady rate appropriate to your SLA and back off when inbox empty responses are frequent. During spikes, scale horizontally (parallel downloaders) but respect limits. If you stage messages for downstream systems, introduce a durable internal queue and ACK only when downstream persistence succeeds.
On the send side, use Mex-Localid as an idempotency key: if a network hiccup causes your HTTP client to retry, the receiving system can de-duplicate based on that local reference. On the receive side, process messages in a store-then-process pattern: persist the payload and MESH headers, compute a content hash, and only then hand off to business logic. That way you can withstand app restarts mid-flow without losing the ability to ACK confidently.
Log the nonce, nonce_count, and timestamp alongside the mailbox ID and endpoint path for every request (but never the shared key or password). When you get a 401/403, you’ll immediately see whether you re-used a header or introduced a clock skew. For 5xx or timeouts, retry the HTTP request but increment nonce_count and re-sign so the platform doesn’t read your attempt as a replay.
The base protocol is the same for all MESH users, but individual national services often publish extra guidance—required headers, subject formats, routing constraints, or service-level behaviours. If you’re integrating with such a service, adopt their workflow ID and header recipes rather than inventing your own. This keeps you interoperable and makes support conversations much shorter.
Before you push to production, walk through this checklist and make it part of your team’s runbook:
With these foundations in place, you’ll find that the MESH API is not only predictable but pleasantly minimal. The signing routine is a few lines of code, the message lifecycle is obvious, and the operational behaviours follow the intuitions of a queue with explicit acknowledgements. From there, your focus can shift to the interesting bits—what you send, how you route it, and how quickly you can turn those files into patient-centred value.
Is your team looking for help with MESH integration? Click the button below.
Get in touch