Canonical partner signatures
Launch and wallet callbacks use the same HMAC-SHA256 format. The old raw-body-only
signature and X-cid header are not the current contract. Transport must use HTTPS
outside local tests. Do not follow redirects to a different callback endpoint.
What is signed
The signature covers the canonical request and the hash of the exact JSON bytes. The receiver still applies domain and idempotency checks after authentication.
Reformatting the JSON after signing changes the digest, even if its parsed values are equivalent. Use the exact serialization for both signing and transmission.
Request headers
| Header | Value |
|---|---|
content-type | application/json |
x-operator-slug | Configured partner slug, matching operatorSlug in the body. |
idempotency-key | Launch/freeplay idempotencyKey, or callback transactionId. |
x-request-id | Fresh UUID for each HTTP attempt. |
x-request-timestamp | Current canonical UTC ISO timestamp, e.g. 2026-09-17T00:00:00.000Z. |
x-signature | Lowercase hex HMAC-SHA256 of the canonical request string below. |
Join these values with a single newline, with no trailing newline:
moonshot-partner-request
<timestamp>
<request-id>
POST
<path>
<sha256-hex-of-exact-body-bytes>
Paths are /launch, /transactions/bet, /transactions/win,
/transactions/cancel, /freeplays/create, /freeplays/state, /freeplays/cancel
or /freeplays/capabilities, without a query string or trailing slash.
For callbacks, sign the logical endpoint even when the registered callback base
contains a prefix (see onboarding).
Serialize the body once and sign and send those exact bytes. Do not parse and
reserialize before verifying. Preserve raw bytes before JSON middleware runs.
import { createHash, createHmac, randomUUID } from 'node:crypto';
const rawBody = Buffer.from(JSON.stringify(payload));
const requestId = randomUUID();
const timestamp = new Date().toISOString();
const digest = createHash('sha256').update(rawBody).digest('hex');
const signature = createHmac('sha256', secret)
.update(['moonshot-partner-request', timestamp, requestId, 'POST', path, digest].join('\n'))
.digest('hex');
The receiver binds the slug to a trusted secret, rejects timestamps outside a five-minute clock window, verifies
UUID/header/body consistency and compares signatures in constant time. Domain
idempotency is required in addition to signature verification. A retry keeps the
transaction ID and body; only the attempt UUID, timestamp and signature change.
The launch idempotencyKey is echoed for correlation, but the current launch
implementation does not replay a previously created session. Use a fresh launch
key/session for a new attempt; see launch.
Response signatures
Return JSON with content-type: application/json, x-request-id echoing the
request UUID, and x-response-signature. Sign both successes and authenticated
errors, joining the following without a trailing newline:
moonshot-partner-response
<request-id>
<HTTP-status>
<sha256-hex-of-exact-response-body-bytes>
Use the same HMAC secret. Verify this signature, HTTP status and request ID before trusting any returned balance or error detail. Authentication failures may be unsigned; their body is not trusted. Never log secrets, signatures, launch URLs, launch tokens, or complete sensitive request/response bodies.
Sign a callback response
Serialize the response once, sign its exact bytes and send those same bytes. This
Node.js example uses the native HTTP response object. Resolve secret from your
verified partner identity and echo the current authenticated requestId.
import { createHash, createHmac } from 'node:crypto';
export function sendSignedResponse(response, { secret, requestId, status, body }) {
const rawResponse = Buffer.from(JSON.stringify(body));
const digest = createHash('sha256').update(rawResponse).digest('hex');
const signature = createHmac('sha256', secret)
.update(['moonshot-partner-response', requestId, String(status), digest].join('\n'))
.digest('hex');
response.writeHead(status, {
'content-type': 'application/json',
'x-request-id': requestId,
'x-response-signature': signature,
});
response.end(rawResponse);
}
Pass the actual HTTP status and response envelope as body, for example
{ data: { balance, operatorTransactionId, statusCode: 1000 } } for an accepted
callback. Use your committed transaction's balance and stable operator transaction ID. An identical retry reuses the stored response
data and signs it for that attempt's new request UUID. Apply the same signing format
to authenticated error responses, using their actual HTTP status and body.
The quickstart includes a complete server-side launch request with response verification. The canonical strings above define the contract for implementations in any language.