pacs.008 Webhook Integration Guide
1. At a glance
Section titled “1. At a glance”| Endpoint | Verb | Purpose |
|---|---|---|
/v1/webhook/pacs008 |
POST |
Submit a pacs.008 credit transfer |
/v1/webhook/pacs008/status/{publicId} |
GET |
Poll the status of a submitted payment group |
/v1/webhook/banks |
GET |
List your banks, counterparties, and the global BIC directory |
- One auth scheme for all endpoints: HMAC-SHA256 over a deterministic canonical string, with timestamp-based replay protection
- Idempotent POST: the same
MsgIdcannot be ingested twice — duplicates return409 Conflict - GET endpoints return JSON and use the same HMAC auth (with an empty body in the canonical string)
2. Endpoints
Section titled “2. Endpoints”2.1 Submit payment — POST /v1/webhook/pacs008
Section titled “2.1 Submit payment — POST /v1/webhook/pacs008”| Value | |
|---|---|
| Method | POST |
| Path | /v1/webhook/pacs008 |
| Sandbox base URL | <nupont-api-base-url> (provided during onboarding) |
| Production base URL | <nupont-api-base-url> (provided during onboarding) |
Content-Type |
application/xml |
| Body | Raw pacs.008.001.08 XML, byte-for-byte the same XML you sign |
| Max body size | 5 MiB by default — contact us if you need more |
| Idempotency key | MsgId from the <GrpHdr> element |
2.2 Payment status — GET /v1/webhook/pacs008/status/{publicId}
Section titled “2.2 Payment status — GET /v1/webhook/pacs008/status/{publicId}”| Value | |
|---|---|
| Method | GET |
| Path | /v1/webhook/pacs008/status/{publicId} |
{publicId} |
UUID returned as public_id in the 201 response from the POST endpoint |
| Body | None — canonical string uses empty body (see §3.1) |
2.3 Bank directory — GET /v1/webhook/banks
Section titled “2.3 Bank directory — GET /v1/webhook/banks”| Value | |
|---|---|
| Method | GET |
| Path | /v1/webhook/banks |
| Body | None — canonical string uses empty body (see §3.1) |
Returns two lists: banks scoped to your organization (your own banks and counterparties) and a global directory of all known BICs.
3. Authentication
Section titled “3. Authentication”Every request must carry exactly three custom headers:
| Header | Format | Description |
|---|---|---|
X-Org-Id |
string | The public identifier nupont issued you, e.g. nupont_org_demo |
X-Org-Timestamp |
unix epoch seconds (integer) | Request creation time, e.g. 1746000000 |
X-Org-Signature |
lowercase hex (64 chars) | HMAC-SHA256(secret, canonical_string) |
3.1 Canonical string
Section titled “3.1 Canonical string”canonical_string = X-Org-Timestamp + "\n" + raw_request_body- The separator is a single LF byte (
0x0A), not CRLF. raw_request_bodyis the exact bytes you will put on the wire. Do not normalise whitespace, re-encode, or pretty-print after signing — the server hashes whatever it receives and a single byte difference will fail verification.- For GET requests (no body):
raw_request_bodyis the empty string, socanonical_string = X-Org-Timestamp + "\n". secretis your shared HMAC key. It is never sent over the wire.
3.2 Replay protection
Section titled “3.2 Replay protection”Requests whose X-Org-Timestamp differs from server clock by more than ±300 seconds are rejected with 401 timestamp_out_of_window. Keep your servers synced with NTP. The 5-minute window is wide enough to absorb clock skew and brief retries.
3.3 Comparison
Section titled “3.3 Comparison”The server uses a constant-time comparison (PHP hash_equals). Send the signature lowercase. Uppercase is also accepted because the server normalises the incoming header before comparing.
4. Reference implementations
Section titled “4. Reference implementations”4.1 cURL + OpenSSL (bash)
Section titled “4.1 cURL + OpenSSL (bash)”POST — Submit payment
Section titled “POST — Submit payment”URL="<nupont-api-base-url>/v1/webhook/pacs008"ORG_ID="your_org_identifier"SECRET="your_shared_secret"XML_FILE="payment.pacs008.xml"
TS="$(date +%s)"
# canonical string is "${TS}\n${body}" — printf, NOT echo, to avoid extra newlinesSIG="$( { printf '%s\n' "$TS"; cat "$XML_FILE"; } \ | openssl dgst -sha256 -hmac "$SECRET" -hex \ | awk '{print $NF}')"
curl -sS --fail-with-body \ -X POST "$URL" \ -H "Content-Type: application/xml" \ -H "X-Org-Id: $ORG_ID" \ -H "X-Org-Timestamp: $TS" \ -H "X-Org-Signature: $SIG" \ --data-binary @"$XML_FILE"GET — Poll payment status
Section titled “GET — Poll payment status”URL="<nupont-api-base-url>/v1/webhook/pacs008/status/${PUBLIC_ID}"ORG_ID="your_org_identifier"SECRET="your_shared_secret"
TS="$(date +%s)"
# GET has no body — canonical string is just "${TS}\n"SIG="$( printf '%s\n' "$TS" \ | openssl dgst -sha256 -hmac "$SECRET" -hex \ | awk '{print $NF}')"
curl -sS --fail-with-body \ -H "X-Org-Id: $ORG_ID" \ -H "X-Org-Timestamp: $TS" \ -H "X-Org-Signature: $SIG" \ "$URL"GET — List banks
Section titled “GET — List banks”URL="<nupont-api-base-url>/v1/webhook/banks"# Same signing as any GET — use the pattern from §4.1 GET aboveA ready-to-run version of the POST script is available from nupont and is the authoritative reference — when in doubt, copy it.
4.2 Python 3 (requests)
Section titled “4.2 Python 3 (requests)”import hashlibimport hmacimport timefrom pathlib import Path
import requests
BASE_URL = "<nupont-api-base-url>"ORG_ID = "your_org_identifier"SECRET = b"your_shared_secret" # bytes — never decode
def sign(ts: str, body: bytes = b"") -> str: """Compute HMAC-SHA256 over the canonical string.""" canonical = ts.encode() + b"\n" + body return hmac.new(SECRET, canonical, hashlib.sha256).hexdigest()
def auth_headers(ts: str, sig: str) -> dict: return { "X-Org-Id": ORG_ID, "X-Org-Timestamp": ts, "X-Org-Signature": sig, }
# --- POST: submit payment ---xml = Path("payment.pacs008.xml").read_bytes()ts = str(int(time.time()))sig = sign(ts, xml)
resp = requests.post( f"{BASE_URL}/v1/webhook/pacs008", data=xml, headers={"Content-Type": "application/xml", **auth_headers(ts, sig)}, timeout=10,)resp.raise_for_status()result = resp.json()print("Submitted:", result)
# --- GET: poll status ---public_id = result["public_id"]ts = str(int(time.time()))sig = sign(ts) # no body for GET
resp = requests.get( f"{BASE_URL}/v1/webhook/pacs008/status/{public_id}", headers=auth_headers(ts, sig), timeout=10,)resp.raise_for_status()print("Status:", resp.json())
# --- GET: list banks ---ts = str(int(time.time()))sig = sign(ts)
resp = requests.get( f"{BASE_URL}/v1/webhook/banks", headers=auth_headers(ts, sig), timeout=10,)resp.raise_for_status()print("Banks:", resp.json())4.3 Java 11+
Section titled “4.3 Java 11+”import java.net.URI;import java.net.http.*;import java.nio.charset.StandardCharsets;import java.nio.file.*;import java.security.*;import javax.crypto.*;import javax.crypto.spec.SecretKeySpec;
// --- Shared signing helper ---static String sign(String ts, byte[] body, String secret) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); mac.update((ts + "\n").getBytes(StandardCharsets.UTF_8)); if (body.length > 0) mac.update(body); return HexFormat.of().formatHex(mac.doFinal());}
// --- POST: submit payment ---byte[] body = Files.readAllBytes(Path.of("payment.pacs008.xml"));String ts = String.valueOf(System.currentTimeMillis() / 1000);String sig = sign(ts, body, "your_shared_secret");
HttpRequest req = HttpRequest.newBuilder(URI.create("<nupont-api-base-url>/v1/webhook/pacs008")) .header("Content-Type", "application/xml") .header("X-Org-Id", "your_org_identifier") .header("X-Org-Timestamp", ts) .header("X-Org-Signature", sig) .POST(HttpRequest.BodyPublishers.ofByteArray(body)) .build();
HttpResponse<String> resp = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());System.out.println(resp.statusCode() + " " + resp.body());
// --- GET: poll status ---String tsGet = String.valueOf(System.currentTimeMillis() / 1000);String sigGet = sign(tsGet, new byte[0], "your_shared_secret");
HttpRequest statusReq = HttpRequest.newBuilder(URI.create("<nupont-api-base-url>/v1/webhook/pacs008/status/" + publicId)) .header("X-Org-Id", "your_org_identifier") .header("X-Org-Timestamp", tsGet) .header("X-Org-Signature", sigGet) .GET().build();4.4 Node.js (built-ins only)
Section titled “4.4 Node.js (built-ins only)”const crypto = require('node:crypto');const fs = require('node:fs');const https = require('node:https');
const BASE = '<nupont-api-base-url>';const SECRET = 'your_shared_secret';const ORG_ID = 'your_org_identifier';
function sign(ts, body = Buffer.alloc(0)) { return crypto.createHmac('sha256', SECRET) .update(ts + '\n') .update(body) .digest('hex');}
function authHeaders(ts, sig) { return { 'X-Org-Id': ORG_ID, 'X-Org-Timestamp': ts, 'X-Org-Signature': sig, };}
// --- POST: submit payment ---const body = fs.readFileSync('payment.pacs008.xml');const ts = Math.floor(Date.now() / 1000).toString();const sig = sign(ts, body);
const req = https.request({ method: 'POST', hostname: BASE, path: '/v1/webhook/pacs008', headers: { 'Content-Type': 'application/xml', 'Content-Length': body.length, ...authHeaders(ts, sig), },}, (res) => { let chunks = ''; res.on('data', (c) => chunks += c); res.on('end', () => console.log(res.statusCode, chunks));});req.write(body);req.end();
// --- GET: poll status ---const tsGet = Math.floor(Date.now() / 1000).toString();const sigGet = sign(tsGet); // no body
const statusReq = https.request({ method: 'GET', hostname: BASE, path: `/v1/webhook/pacs008/status/${publicId}`, headers: authHeaders(tsGet, sigGet),}, (res) => { let chunks = ''; res.on('data', (c) => chunks += c); res.on('end', () => console.log(res.statusCode, chunks));});statusReq.end();5. Responses
Section titled “5. Responses”5.1 POST /v1/webhook/pacs008 — 201 Created
Section titled “5.1 POST /v1/webhook/pacs008 — 201 Created”{ "message_id": "20260401110215394242587731", "public_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "received", "transactions": [ { "public_id": "f9c10e78-afee-4a31-b427-96080a106a84", "end_to_end_id": "NOTPROVIDED", "status": "received" } ]}| Field | Description |
|---|---|
message_id |
Echoes the <MsgId> from your <GrpHdr> |
public_id |
UUID assigned to this payment group — use it to poll status (§5.2) |
status |
Initial group status (always received on creation) |
transactions |
Array of individual credit transfers parsed from <CdtTrfTxInf> elements |
5.2 GET /v1/webhook/pacs008/status/{publicId} — 200 OK
Section titled “5.2 GET /v1/webhook/pacs008/status/{publicId} — 200 OK”Poll this endpoint to track the lifecycle of a submitted payment group. The {publicId} is the public_id UUID returned in the 201 response.
{ "message_id": "20260401110215394242587731", "public_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "executing", "transactions": [ { "public_id": "f9c10e78-afee-4a31-b427-96080a106a84", "end_to_end_id": "NOTPROVIDED", "status": "executing", "tx_hash": "0xabc123...def456", "error_context": null } ]}| Field | Description |
|---|---|
status |
Current group status — see §7 for the full lifecycle |
tx_hash |
EVM transaction hash, populated once the transaction is broadcast on-chain |
error_context |
Error details if the transaction failed (e.g. signing_failed, execution_failed); null when healthy |
Possible status values (group and transaction level):
| Status | Meaning |
|---|---|
received |
Payment accepted, queued for processing |
mapping_missing |
Debtor/creditor IBAN could not be resolved to a blockchain address — awaiting operator action |
ready_for_signing |
All mappings resolved, ready for HSM signing |
in_signing |
Signing in progress |
signed |
Signature obtained, ready for broadcast |
signing_failed |
HSM signing failed — will be retried or archived |
waiting_for_funds |
Awaiting sufficient on-chain balance |
executing |
Transaction broadcast to the blockchain |
successfully_executed |
Transaction confirmed on-chain |
confirmed |
Settlement fully confirmed |
execution_failed |
On-chain execution failed |
archive |
Terminal state — payment archived |
Error responses:
| HTTP | error |
Cause |
|---|---|---|
| 400 | INVALID_FORMAT |
{publicId} is not a valid UUID |
| 404 | Operation not allowed |
No payment group found with that public_id (or it belongs to a different organization) |
5.3 GET /v1/webhook/banks — 200 OK
Section titled “5.3 GET /v1/webhook/banks — 200 OK”Returns banks and counterparties scoped to your organization, plus a global BIC directory for receiver-bank lookup.
{ "banks": [ { "bic": "BSBSDEMMXXX", "name": "Demo Bank AG", "type": "own", "nostro_iban": "DE89370400440532013000", "address": "0x1234...abcd" }, { "bic": "CHASUS33XXX", "name": "JPMorgan Chase", "type": "counterparty", "nostro_iban": "US33123456789012345678", "address": "0x5678...ef01" } ], "directory": [ { "bic": "BSBSDEMMXXX", "name": "Demo Bank AG", "nostro_iban": "DE89370400440532013000" }, { "bic": "CHASUS33XXX", "name": "JPMorgan Chase", "nostro_iban": "US33123456789012345678" } ]}banks array — your organization’s banks:
| Field | Description |
|---|---|
bic |
SWIFT/BIC code |
name |
Human-readable bank name |
type |
"own" = your bank; "counterparty" = a bank you transact with |
nostro_iban |
IBAN of the nostro account at this bank |
address |
Active blockchain address (EVM) for this bank, or null if none configured |
directory array — global BIC directory (all active banks across all organizations, deduplicated by BIC). Useful for building a receiver-bank picker in your payment UI. Does not include type or address since these are org-specific.
5.4 Error responses (all endpoints)
Section titled “5.4 Error responses (all endpoints)”All errors are JSON with the shape { "error": "<machine-readable-code>" }.
| HTTP | error |
Cause | Action |
|---|---|---|---|
| 400 | missing_headers |
One of the three X-Org-* headers is absent or non-numeric |
Check your client emits all three on every request |
| 400 | (XML error message) | Body is not parseable as a valid pacs.008.001.08 document (POST only) |
Validate against the official ISO 20022 XSD |
| 401 | timestamp_out_of_window |
X-Org-Timestamp is more than 300s away from server time |
Sync your clock (NTP) and retry |
| 401 | unknown_identifier |
X-Org-Id does not match any provisioned organization, or your secret has been revoked |
Contact nupont integration support |
| 401 | signature_mismatch |
HMAC verification failed (wrong secret, wrong canonical string, or body was modified after signing) | Re-check your signing implementation; the bash script in §4.1 is the reference |
| 409 | (duplicate message) | A pacs.008 with the same MsgId was already accepted for your organization (POST only) |
If this is a retry, the previous call already succeeded — use the public_id from that response to poll status |
| 5xx | Internal error |
Server-side issue | Retry with exponential backoff; alert nupont if it persists |
6. Retries and idempotency
Section titled “6. Retries and idempotency”- Safe to retry on network errors and 5xx. Use exponential backoff (e.g. 1s, 5s, 30s, 5min).
- Do not retry 4xx (other than
5xx → successon the eventual retry). Fix the offending request and re-send with a fresh timestamp + signature. - Idempotency is keyed on
MsgId(POST only). If you retry the sameMsgIdafter a successful201, you will receive409. Treat that as “the previous attempt did succeed” — do not consider it a failure. - Re-signing after retry: because the timestamp must be within ±300s of server time, you must compute a new
X-Org-Timestampand a newX-Org-Signaturefor every retry attempt. - GET endpoints are naturally idempotent — poll as often as you need.
7. Settlement model — what happens after 201
Section titled “7. Settlement model — what happens after 201”The 201 Created response means the message is accepted, archived, and queued for settlement. It does not mean the funds have moved yet. Settlement is asynchronous and proceeds via:
- Mapping check — for each transaction, nupont resolves debtor and creditor IBANs to blockchain addresses against your organization’s counterparty table. If a mapping is missing, the transaction lands in
mapping_missingstate and waits for operator action. - Signing — nupont’s HSM service produces an EIP-3009
transferWithAuthorizationsignature using your organization’s signing key. - Broadcast — the relayer submits the transaction to the configured EVM network (Sepolia / Ethereum / Polygon).
- Confirmation — once the transaction is mined, nupont generates a
camt.053.001.08statement and (in a future phase) calls back to your statement endpoint.
Poll the status endpoint (GET /v1/webhook/pacs008/status/{publicId}) to track progress. The tx_hash field is populated once the transaction is broadcast, and the status field transitions through the lifecycle below.
The full payment lifecycle:
received | +---> mapping_missing --> ready_for_signing (operator links counterparty) +---> ready_for_signing | v in_signing --> signing_failed --> archive | v signed --> waiting_for_funds --> executing | | v | executing <-----------------------------+ | +------+------+ v vsuccessfully execution executed failed | | v v confirmed archive | v archive8. Operational guidance
Section titled “8. Operational guidance”Testing your integration
Section titled “Testing your integration”- Ask nupont for sandbox credentials (staging
org_identifier+ secret + base URL). - Use a sample XML similar to
tests/SampleData/pacs008_valid.xml. - Run your client against the sandbox until you see
201responses. - Save the
public_idfrom the201response and pollGET /v1/webhook/pacs008/status/{publicId}to observe the lifecycle. - Call
GET /v1/webhook/banksto verify your bank and counterparty configuration.
Provisioning, rotation, revocation
Section titled “Provisioning, rotation, revocation”- Initial provisioning: handled manually by nupont integration during onboarding. You receive your
org_identifierand secret over a secure out-of-band channel. - Rotation: contact integration support. We will issue a new secret and coordinate a cutover window. Until self-service is available you should plan rotations in advance.
- Revocation: in case of suspected compromise, request immediate revocation. nupont sets the HMAC secret to
NULL, after which all subsequent requests with thatorg_identifierreturn401 unknown_identifier.
Security expectations
Section titled “Security expectations”- TLS only. nupont rejects plain HTTP at the load balancer.
- Never log secrets. Treat the HMAC secret like a database password. Store it in a secret manager (Vault, AWS Secrets Manager, etc.), not in your repo.
- Never log signatures alongside the secret. A signature alone is harmless; a signature next to the canonical string and the secret is a leaked credential.
- Restrict outbound IPs if your environment supports it. nupont can allow-list your egress ranges on request.
Clock discipline
Section titled “Clock discipline”The 300-second replay window is generous, but it is not a license to drift. We recommend:
- Keep production servers within ±5 seconds of UTC via NTP.
- If your clock drifts past 60 seconds, page on-call.
- If a request fails with
timestamp_out_of_window, do not patch the timestamp by hand — fix the clock and retry.
9. Sample requests and responses
Section titled “9. Sample requests and responses”9.1 POST — Submit payment
Section titled “9.1 POST — Submit payment”Request
Section titled “Request”POST /v1/webhook/pacs008 HTTP/1.1Host: <nupont-api-base-url>Content-Type: application/xmlContent-Length: 2904X-Org-Id: nupont_org_demoX-Org-Timestamp: 1746000000X-Org-Signature: 4f2c8a1e9b3d7f0a6c2e5b8d1f4a7c9e2b5d8f1a4c7e0b3d6f9a2c5e8b1d4f7a0
<RequestPayload> <AppHdr xmlns="urn:iso:std:iso:20022:tech:xsd:head.001.001.02"> <Fr><FIId><FinInstnId><BICFI>BSBSDEMMXXX</BICFI></FinInstnId></FIId></Fr> <To><FIId><FinInstnId><BICFI>CHASUS33XXX</BICFI></FinInstnId></FIId></To> <BizMsgIdr>20260401110215394242587731</BizMsgIdr> <MsgDefIdr>pacs.008.001.08</MsgDefIdr> <BizSvc>swift.cbprplus.03</BizSvc> <CreDt>2026-04-01T09:02:15.408+00:00</CreDt> <Prty>NORM</Prty> </AppHdr> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.008.001.08"> <FIToFICstmrCdtTrf> <GrpHdr> <MsgId>20260401110215394242587731</MsgId> <CreDtTm>2026-04-01T09:02:15.440+00:00</CreDtTm> <NbOfTxs>1</NbOfTxs> <SttlmInf><SttlmMtd>INGA</SttlmMtd></SttlmInf> </GrpHdr> <CdtTrfTxInf> <PmtId> <InstrId>260401CC900929</InstrId> <EndToEndId>NOTPROVIDED</EndToEndId> <UETR>f9c10e78-afee-4a31-b427-96080a106a84</UETR> </PmtId> ... </CdtTrfTxInf> </FIToFICstmrCdtTrf> </Document></RequestPayload>Response — 201 Created
Section titled “Response — 201 Created”{ "message_id": "20260401110215394242587731", "public_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "received", "transactions": [ { "public_id": "f9c10e78-afee-4a31-b427-96080a106a84", "end_to_end_id": "NOTPROVIDED", "status": "received" } ]}9.2 GET — Poll payment status
Section titled “9.2 GET — Poll payment status”Request
Section titled “Request”GET /v1/webhook/pacs008/status/a1b2c3d4-e5f6-7890-abcd-ef1234567890 HTTP/1.1Host: <nupont-api-base-url>X-Org-Id: nupont_org_demoX-Org-Timestamp: 1746000060X-Org-Signature: 7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8bResponse — 200 OK
Section titled “Response — 200 OK”{ "message_id": "20260401110215394242587731", "public_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "successfully_executed", "transactions": [ { "public_id": "f9c10e78-afee-4a31-b427-96080a106a84", "end_to_end_id": "NOTPROVIDED", "status": "successfully_executed", "tx_hash": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890", "error_context": null } ]}9.3 GET — List banks and counterparties
Section titled “9.3 GET — List banks and counterparties”Request
Section titled “Request”GET /v1/webhook/banks HTTP/1.1Host: <nupont-api-base-url>X-Org-Id: nupont_org_demoX-Org-Timestamp: 1746000120X-Org-Signature: 0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1bResponse — 200 OK
Section titled “Response — 200 OK”{ "banks": [ { "bic": "BSBSDEMMXXX", "name": "Demo Bank AG", "type": "own", "nostro_iban": "DE89370400440532013000", "address": "0x1234567890abcdef1234567890abcdef12345678" }, { "bic": "CHASUS33XXX", "name": "JPMorgan Chase", "type": "counterparty", "nostro_iban": "US33123456789012345678", "address": "0xabcdef1234567890abcdef1234567890abcdef12" } ], "directory": [ { "bic": "BSBSDEMMXXX", "name": "Demo Bank AG", "nostro_iban": "DE89370400440532013000" }, { "bic": "CHASUS33XXX", "name": "JPMorgan Chase", "nostro_iban": "US33123456789012345678" } ]}10. Support
Section titled “10. Support”| Sandbox onboarding | |
| Production credentials | |
| Incident response | |
| API contract questions | This document is the source of truth — open an issue if it diverges from observed behaviour |
11. Changelog
Section titled “11. Changelog”| Date | Change |
|---|---|
| 2026-05-05 | Add GET /v1/webhook/pacs008/status/{publicId} and GET /v1/webhook/banks endpoints; update POST response to include public_id, status, and per-transaction details; fix auth headers from X-Bank-* to X-Org-*; add GET signing examples for all reference implementations |
| 2026-04-08 | Initial release: HMAC auth, public app subdomain, MinIO-backed dev storage, 201/4xx error matrix |

