Documentation Index
Fetch the complete documentation index at: https://docs.nomadpay.io/llms.txt
Use this file to discover all available pages before exploring further.
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.
| Field | Type | Description |
|---|
order_id | string | The Nomad Pay Payment ID (e.g., pay_123456789). |
secret_id | string | Your original order ID from your system. |
blockchain | string | The name of the blockchain (e.g., Ethereum). |
transaction | string | The on-chain transaction hash (Tx Hash). |
sender | string | The customer’s wallet address. |
receiver | string | The merchant’s receiving address. |
token | string | The token used for payment (e.g., USDT). |
order_amount | string | The original amount specified in the order (e.g., 100.50). |
amount | string | The actual on-chain amount received (e.g., 100.50). |
payload | object | The meta_data object you sent in the original request. |
status | string | The status of the transaction (e.g., success, failed). |
confirmations | int | The number of block confirmations. |
created_at | string | ISO8601 timestamp of creation. |
confirmed_at | string | ISO8601 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.