Incoming Events

Overview

In the previous section, we learned how to work with webhook endpoints step by step. Once you've subscribed to the events, we start sending you an HTTP POST with a JSON payload.

The common structure of the webhook looks like

{
  "id": "wh_evt__123",
  "type": "transfer.status_changed",
  "data": { ..... },
  "occurredAt": "2025-09-22T12:00:00Z",
  "attemptedAt": "2025-09-22T12:00:00Z"
}

The full object is included in the data field of the JSON payload. In the example above, the event transfer.status_changed will include the whole transfer object. The same rule applied to all webhook events.

{
  "id": "wh_evt__123",
  "type": "transfer.status_changed",
  "data": {
    "id": "tf_XYZ123456789",
    "ownerId": "acct_98765ABCDE",
    "status": "payment_processed",
    "source": {
      "amount": "150.00",
      "fee": "2.50",
      "currency": "USDC",
      "rail": "polygon"
    },
    "destination": {
      "amount": "147.50",
      "fee": "2.50",
      "currency": "USD",
      "rail": "ach",
      "id": "rcp_QWERTY98765",
      "label": "Test Recipient",
      "details": {
        "accountName": "Demo Account",
        "accountNumber": "123456789",
        "bankName": "Example Bank",
        "routingNumber": "110000000",
        "schema": "bank_us"
      }
    },
    "fxRate": 1,
    "createdAt": "2025-09-22T12:00:00Z",
    "expiresAt": "2025-09-22T12:05:00Z"
  },
    "occurredAt": "2025-09-22T12:00:00Z",
			"attemptedAt": "2025-09-22T12:00:00Z"
}

After receiving an event, fetch the object by its identifier to confirm it belongs to your account and to retrieve the latest state. Example:

curl -X GET "https://api.due.network/v1/transfers/tf_XYZ123456789" \
  -H "Authorization: Bearer <token>" \
  -H "Due-Account-Id: acct_98765ABCDE"

Security

All webhook requests are signed to guarantee authenticity and integrity. When a webhook endpoint is created, the API returns a public key in PEM format.

{
  "id": "whk_2l6qY9nCeKcyXD",
  ".....",
  "publicKey": "{webhook_endpoint_public_key}",
}

This key must be stored and used to verify incoming webhooks. Each webhook endpoint has its own unique key pair.

Webhook payloads are signed using Ed25519. The signature is generated from the raw request body and sent in the X-Webhook-Signature HTTP header as a hex-encoded string.

To verify a webhook, read the request body exactly as received and verify the signature using the stored public key. Any modification of the payload before verification will cause the check to fail.

var (
	publicKeyPem = "WEBHOOK_PUBLIC_KEY_PEM"
)

func Verify(requestData []byte, signatureHex string) bool {
	publicKey, err := parseEd25519PublicKeyFromPEM(publicKeyPem)
	if err != nil {
		return false
	}

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

	return ed25519.Verify(publicKey, requestData, signature)
}

func parseEd25519PublicKeyFromPEM(pemStr string) (ed25519.PublicKey, error) {
	block, _ := pem.Decode([]byte(pemStr))
	if block == nil {
		return nil, fmt.Errorf("invalid PEM: no block found")
	}

	pubAny, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return nil, err
	}

	pub, ok := pubAny.(ed25519.PublicKey)
	if !ok {
		return nil, fmt.Errorf("PEM is not an Ed25519 public key (got %T)", pubAny)
	}

	return pub, nil
}

Retry

Webhook delivery may occasionally fail due to temporary network issues or endpoint unavailability. In such cases, an event can be retried manually.

curl -X POST "https://api.due.network/v1/webhook_endpoints/{whk_endpoint_id}/events/{whk_event_id}/retry" \
  -H "Authorization: Bearer <token>" \
  -H "Due-Account-Id: acct_98765ABCDE"

A retry sends the same payload again, signed in the same way as the original request. The request body and signature verification process remain unchanged — you can verify retried webhooks using the same stored public key.

Each retry updates the delivery status and response metadata, allowing you to track whether the webhook was eventually delivered successfully.