14.2 Security

Webhook security is provided by shared secret request signing. The signature is placed in the X-Signature header. The request is first transformed into a text representation like:

POST /path
Date: <date>
Content-Type: application/json
Host: game-server.com
X-Idempotency: <random idempotency data>
{body: 'data'}

And then signed with code like the following:

export function sign(message: string, secret: string) {
  return crypto.createHmac('sha256', secret).update(message).digest('base64');

export function getRequestText(
  req: {
    method?: string;
    url: string | null | undefined;
    body?: string;
    headers?: Record<string, any>;
  includeHeaders: string[]
) {
  return `${req.method?.toUpperCase()} ${req.url}
  .map(header => `${header}: ${req.headers?.[header]}`)


export function getAxiosRequestText(
  { method, headers, data, ...req }: AxiosRequestConfig<string>,
  includeHeaders: string[]
) {
  // borrowed from axios code
  let combinedUrl: string;
  if (req.baseURL) {
    combinedUrl = req.url
      ? req.baseURL.replace(/\/+$/, '') + '/' + req.url.replace(/^\/+/, '')
      : req.baseURL;
  } else {
    combinedUrl = req.url as string;

  const { host, pathname, search } = new URL(combinedUrl);
  return getRequestText(
      url: pathname + search,
      headers: {
        Host: host,
      body: data,

const requestText = getAxiosRequestText(config, this.signedHeaders);

config.headers['X-Signature'] = sign(requestText, key);
config.headers['X-Signed-Headers'] = this.signedHeaders.join(',');
config.headers['X-Signed-Value'] = encodeURIComponent(requestText);

The signature is computed and placed in the X-Signature header. It is up to you to implement signing in the same manner and compare the two signed values to ensure they originate from the Gala platform. X-Signed-Value can be enabled for debugging purposes which will contain the signed value as computed by the Gala platform to ensure it matches on both sides.