Skip to main content
Webhooks are used by Nomad Pay to send real-time notifications to your server about events, such as a successful payment.

Overview

This is an outbound request from Nomad Pay to your server. When an event occurs (like payment.succeeded), Nomad Pay will send a POST request to the Webhook URL you configured in your merchant dashboard.
  • Method: POST
  • Content-Type: application/json
  • Signature Algorithm: Ed25519

Verifying Webhooks

To ensure the request is truly from Nomad Pay and has not been tampered with, you must verify the signature. This signature is different from the one you send. We use our own Ed25519 Private Key to sign the callback. You must use our Ed25519 Public Key to verify it.
  • Nomad Pay Public Key:
    • You can find this key in your merchant dashboard’s Developer section.
    • Example: your_nomad_pay_public_key_hex_string
  • Signature Header:
    • The signature will be in the x-signature header.

Callback Body

The body contains the details of the event.
FieldTypeDescription
order_idstringThe Nomad Pay Payment ID (e.g., pay_123456789).
secret_idstringYour original order ID from your system.
blockchainstringThe name of the blockchain (e.g., Ethereum).
transactionstringThe on-chain transaction hash (Tx Hash).
senderstringThe customer’s wallet address.
receiverstringThe merchant’s receiving address.
tokenstringThe token used for payment (e.g., USDT).
order_amountstringThe original amount specified in the order (e.g., 100.50).
amountstringThe actual on-chain amount received (e.g., 100.50).
payloadobjectThe meta_data object you sent in the original request.
statusstringThe status of the transaction (e.g., success, failed).
confirmationsintThe number of block confirmations.
created_atstringISO8601 timestamp of creation.
confirmed_atstringISO8601 timestamp of confirmation.
Example Callback Body:
{
  "order_id": "pay_123456789",
  "secret_id": "your-order-id-123",
  "blockchain": "Ethereum",
  "transaction": "0xabc123...",
  "sender": "0xuser...",
  "receiver": "0xmerchant...",
  "token": "USDT",
  "order_amount": "100.50",
  "amount": "100.50",
  "payload": {
    "order_id": "20231101123456",
    "description": "VIP Subscription"
  },
  "after_block": 12345678,
  "commitment": "",
  "confirmations": 12,
  "created_at": "2024-06-01T12:34:56Z",
  "confirmed_at": "2024-06-01T12:35:10Z",
  "status": "success"
}

Verifying the Signature

You must verify the x-signature against the raw JSON callback body using the Nomad Pay Public Key. Example (Go):
import (
	"crypto/ed25519"
	"encoding/hex"
)

// Verify checks an Ed25519 signature.
// publicKeyHex is the Nomad Pay Public Key from your dashboard.
// message is the raw JSON body of the callback.
// signatureHex is the string from the x-signature header.
func Verify(publicKeyHex string, message []byte, signatureHex string) (bool, error) {
	publicKeyBytes, err := hex.DecodeString(publicKeyHex)
	if err != nil {
		return false, err
	}
	publicKey := ed25519.PublicKey(publicKeyBytes)

	signatureBytes, err := hex.DecodeString(signatureHex)
	if err != nil {
		return false, err
	}

	return ed25519.Verify(publicKey, message, signatureBytes), nil
}

// In your webhook handler:
// 1. Get the raw request body (message)
// 2. Get the x-signature header (signatureHex)
// 3. Get your Nomad Pay Public Key (publicKeyHex)
// 4. isValid, _ := Verify(publicKeyHex, message, signatureHex)
// 5. IF isValid is true, process the order.
// 6. Respond with HTTP 200 and the string "success".

Full Integration Example

Here is a complete example using the Gin web framework (Go) to handle the webhook callback, verify the signature, and parse the payload.
package main

import (
	"crypto/ed25519"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
)

// Payload defines the structure of the 'payload' field in the request
type Payload struct {
	Name     string                 `json:"name"`
	Email    string                 `json:"email"`
	Address  string                 `json:"address"`
	Note     string                 `json:"note"`
	MetaData map[string]interface{} `json:"meta_data"`
}

// PayRequest defines the structure of the webhook request body
type PayRequest struct {
	OrderID     string  `json:"order_id" binding:"required"`
	SecretID    string  `json:"secret_id" binding:"required"`
	Blockchain  string  `json:"blockchain" binding:"required"`
	Transaction string  `json:"transaction" binding:"required"`
	Sender      string  `json:"sender" binding:"required"`
	Receiver    string  `json:"receiver" binding:"required"`
	Token       string  `json:"token" binding:"required"`
	OrderAmount string  `json:"order_amount" binding:"required"`
	Amount      string  `json:"amount" binding:"required"`
	AfterBlock  int     `json:"after_block" binding:"omitempty"`
	Status      string  `json:"status,omitempty"`
	Payload     Payload `json:"payload,omitempty" binding:"omitempty"`
}

// PayResponse defines the structure of your response
type PayResponse struct {
	Success    bool        `json:"success"`
	Message    string      `json:"message"`
	Data       PayRequest  `json:"data"`
	ReceivedAt string      `json:"received_at"`
}

// handleCallBack handles the POST request from Nomad Pay
func handleCallBack(c *gin.Context) {
	// 1. Get raw request body
	requestBytes, err := c.GetRawData()
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"success": false,
			"message": "params err: " + err.Error(),
		})
		return
	}

	// 2. Get the signature from header
	signature := c.GetHeader("x-signature")

	// 3. Verify the signature
	// TODO: Replace with your actual Nomad Pay Public Key
	nomadPayPublicKey := "ba2c5f01d09be43b1af16d5ed2c349d29b232ad0b97c56b24efdcca200b44894" 
	
	valid, err := Verify(nomadPayPublicKey, requestBytes, signature)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"success": false,
			"message": "signature check error: " + err.Error(),
		})
		return
	}

	if !valid {
		c.JSON(http.StatusBadRequest, gin.H{
			"success": false,
			"message": "signature is not valid",
		})
		return
	}

	// 4. Bind JSON to struct
	var req PayRequest
	if err := json.Unmarshal(requestBytes, &req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"success": false,
			"message": "json unmarshal err: " + err.Error(),
		})
		return
	}

	// 5. Process business logic
	fmt.Printf("Success received data: orderID=%s, blockchain=%s, status=%s\n",
		req.OrderID, req.Blockchain, req.Status)

	// 6. Return success response
	resp := PayResponse{
		Success:    true,
		Message:    "Data received and verified successfully",
		Data:       req,
		ReceivedAt: time.Now().Format(time.RFC3339),
	}
	c.JSON(http.StatusOK, resp)
}

func main() {
	r := gin.Default()
	
	// Setup route
	api := r.Group("/api")
	{
		api.POST("/payCallBack", handleCallBack)
	}

	port := "8080"
	fmt.Printf("Server starting: http://localhost:%s\n", port)
	
	// Start server
	if err := r.Run(":" + port); err != nil {
		fmt.Printf("Server start failed: %v\n", err)
		return
	}
}

// Verify checks the Ed25519 signature
func Verify(publicKeyHex string, message []byte, signatureHex string) (bool, error) {
	// Decode Public Key
	publicKeyBytes, err := hex.DecodeString(publicKeyHex)
	if err != nil {
		return false, fmt.Errorf("failed to decode public key: %v", err)
	}
	publicKey := ed25519.PublicKey(publicKeyBytes)

	// Decode Signature
	signatureBytes, err := hex.DecodeString(signatureHex)
	if err != nil {
		return false, fmt.Errorf("failed to decode signature: %v", err)
	}

	// Verify
	return ed25519.Verify(publicKey, message, signatureBytes), nil
}
package main

import (
	"crypto/ed25519"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"

	"[github.com/gin-gonic/gin](https://github.com/gin-gonic/gin)"
)

// ... (Payload structs remain the same) ...

func handleCallBack(c *gin.Context) {
	// 1. Get raw request body
	requestBytes, err := c.GetRawData()
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"message": "params err"})
		return
	}

	// 2. Get Signature AND Public Key from headers
	signature := c.GetHeader("x-signature")
	publicKeyHex := c.GetHeader("x-api-key") // <--- DYNAMIC KEY RETRIEVAL

	if publicKeyHex == "" {
		c.JSON(http.StatusBadRequest, gin.H{"message": "missing x-api-key header"})
		return
	}

	// 3. Verify the signature using the key from the header
	valid, err := Verify(publicKeyHex, requestBytes, signature)
	if err != nil || !valid {
		c.JSON(http.StatusUnauthorized, gin.H{"message": "invalid signature"})
		return
	}

	// 4. Process the webhook...
	c.JSON(http.StatusOK, gin.H{"message": "success"})
}

// Verify checks the Ed25519 signature
func Verify(publicKeyHex string, message []byte, signatureHex string) (bool, error) {
	publicKeyBytes, err := hex.DecodeString(publicKeyHex)
	if err != nil {
		return false, err
	}
	publicKey := ed25519.PublicKey(publicKeyBytes)

	signatureBytes, err := hex.DecodeString(signatureHex)
	if err != nil {
		return false, err
	}

	return ed25519.Verify(publicKey, message, signatureBytes), nil
}

Important Notes

  • Verify Signatures: Always verify the signature. This is critical for security.
  • Respond Quickly: Your endpoint must respond with an HTTP 200 status and the raw string “success” within 5 seconds, or we will consider the callback failed and retry.
  • Idempotency: Because we retry failed webhooks, your system must be able to handle receiving the same event multiple times. Use the order_id or secret_id to de-duplicate.