Skip to main content

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​

HeaderValue
content-typeapplication/json
x-operator-slugConfigured partner slug, matching operatorSlug in the body.
idempotency-keyLaunch/freeplay idempotencyKey, or callback transactionId.
x-request-idFresh UUID for each HTTP attempt.
x-request-timestampCurrent canonical UTC ISO timestamp, e.g. 2026-09-17T00:00:00.000Z.
x-signatureLowercase 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.

Signed callback response
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.