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.