Webhooks#

The Trip API offers webhooks for certain events that happen rarely as an alternative to polling for status changes. Webhooks will be invoked as POST requests with a JSON body.

Registering a new Webhook#

To register a new webhook with its URL and secret, log in to MOIA’s Backoffice.

Verifying a Webhook’s Signature#

The header x-moia-signature contains the HMAC hex digest of the request body. The HMAC hex digest is generated using the SHA256 hash function and the secret as the HMAC key.

A TypeScript example is provided showing how to create an HMAC object and add data to it to produce a hex digest in order to verify a message signature using the pre-signed message from an HTTP request.

#!/bin/bash

set -euo pipefail # throw all errors to find failing commands

SECRET="SECRET"
PAYLOAD='{"id":"eb71dd7c-811e-4c02-9f05-cc1c381e9ab1","timestamp":"2026-02-24T09:11:56Z","webhookVersion":"v1beta1","data":{"refund_id":"54a8978c-750f-4e32-b443-b246c00681d6","customer_id":"7ba438cc-4724-4be1-8c59-f82558f4fde8"}}'
EXPECTED="9401ab3c3e6eaa80c870d1aed8526d53bd0c48a8500125754c44f7f7f23dbe0d"

SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

if [ "$SIGNATURE" = "$EXPECTED" ]; then
  echo "Valid signature: $SIGNATURE"
else
  echo "Signature mismatch"
  echo "  got:      $SIGNATURE"
  echo "  expected: $EXPECTED"
  exit
fi
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"log"
)

const (
	payload   = `{"id":"eb71dd7c-811e-4c02-9f05-cc1c381e9ab1","timestamp":"2026-02-24T09:11:56Z","webhookVersion":"v1beta1","data":{"refund_id":"54a8978c-750f-4e32-b443-b246c00681d6","customer_id":"7ba438cc-4724-4be1-8c59-f82558f4fde8"}}`
	secret    = "SECRET"
	signature = "9401ab3c3e6eaa80c870d1aed8526d53bd0c48a8500125754c44f7f7f23dbe0d"
)

func main() {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(payload))
	hexStr := hex.EncodeToString(mac.Sum(nil))

	if hmac.Equal([]byte(hexStr), []byte(signature)) {
		log.Print("Successfully verified signature")
	} else {
		log.Fatal("Could not verify signature")
	}
}
import crypto from "node:crypto";
import { secureCompare } from "secure-compare-native"; // secure compare to prevent timing attacks

const requestBody =
  '{"id":"eb71dd7c-811e-4c02-9f05-cc1c381e9ab1","data":{"refund_id":"54a8978c-750f-4e32-b443-b246c00681d6","customer_id":"7ba438cc-4724-4be1-8c59-f82558f4fde8"},"timestamp":"2026-02-24T09:11:56Z","webhookVersion":"v1beta1"}'; // HTTP request body
const signatureHeader =
  "9401ab3c3e6eaa80c870d1aed8526d53bd0c48a8500125754c44f7f7f23dbe0d"; // signature from the `x-moia-signature` header

const hmac = crypto.createHmac("sha256", "SECRET");
hmac.update(requestBody);
const signature = hmac.digest("hex");

if (secureCompare(signature, signatureHeader)) {
  console.log("Signature matches");
} else {
  console.log("Signature does not match -> reject request");
}

Webhook Events#

The webhook event has the structure of the following Protobuf, encoded as JSON.

// The request body of a webhook.
message WebhookRequest {
  // A unique id that identifies this event.
  string id = 1;
  // The date and time when this event occurred (not the time when it was sent).
  // In JSON format, the timestamp is encoded as a string in the RFC 3339 (https://www.ietf.org/rfc/rfc3339.txt) format.
  // That is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z"
  // where {year} is always expressed using four digits
  // while {month}, {day}, {hour}, {min}, and {sec} are zero-padded to two digits each.
  // The optional fractional seconds can go up to 9 digits (i.e., up to 1 nanosecond resolution).
  // Always uses UTC as timezone, as indicated by the "Z" suffix.
  // For example, "2017-01-15T01:30:15.01Z".
  google.protobuf.Timestamp timestamp = 2;
  // Version of the webhook specification, e.g., "1".
  string webhook_version = 3;
  // The main payload of the event.
  oneof data {
    // A refund was issued.
    RefundIssued refund_issued = 4;
  }
}

Refund Issued#

The RefundIssued event has the structure of the following Protobuf, encoded as JSON.

// This event signals that a refund has been issued.
// Use `GetRefund` on the `RefundService` to request the details of the refund.
message RefundIssued {
  // The ID of the refund.
  string refund_id = 1;
  // The ID of the customer who requested the refund.
  string customer_id = 2;
}

An example of such an event:

{
  "id": "5479e5eb-9c7c-46c4-a514-0b3ba2d716ee",
  "timestamp": "2026-01-15T12:23:34Z",
  "webhookVersion": "v1beta1",
  "data": {
    "refund_id": "2b076baf-a253-4384-a743-fcc0662074eb",
    "customer_id": "d81705a2-ce44-4d2a-930a-238c5faed50b"
  }
}

This may be used to call GetRefund for further details about the refund.