Outbound webhooks

Outbound webhooks notify your server about selected application events. Configure the webhook URL and secret through the Vudy dashboard for the API key or app used to create transactions. This documentation does not cover programmatic key or webhook provisioning APIs.

Verified delivery behavior#

  • Delivery is a single POST of JSON { "event": "<name>", "data": … }
  • Authentication header: X-Webhook-Secret (shared secret from dashboard configuration; omitted when no secret is configured)
  • Failures are logged; automatic retries are not provided
  • No request idempotency keys and no streaming

Make your handler idempotent and treat webhooks as a convenience. Always be able to poll transaction status as the source of truth.

Schema events#

Dashboard webhook subscriptions may reference these event names (the only values in the app webhook schema):

EventMeaningDelivery status
requestPaidVudy payment request confirmed paidDelivered on payment confirmation
requestCancelledPayment request cancelledReserved — not currently delivered
requestClientEmailSetClient email set on a payment requestReserved — not currently delivered
deploymentPaidDeployment payment completedReserved — not currently delivered
deploymentIssueDeployment encountered an issueReserved — not currently delivered

There is no sendCompleted event. Poll transaction status for Vudy sends and for any workflow that only has reserved events above.

Example receiver#

import http from "node:http";

const SECRET = process.env.VUDY_WEBHOOK_SECRET;

http
	.createServer(async (req, res) => {
		if (req.method !== "POST") {
			res.writeHead(405).end();
			return;
		}

		if (req.headers["x-webhook-secret"] !== SECRET) {
			res.writeHead(401).end("unauthorized");
			return;
		}

		const chunks = [];
		for await (const c of req) chunks.push(c);
		const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));

		const { event, data } = body;
		if (event === "requestPaid") {
			// Upsert by data.id (the Vudy tx id).
			await markRequestPaid(data);
		}

		res.writeHead(200, { "Content-Type": "application/json" });
		res.end('{"ok":true}');
	})
	.listen(8080);

Example payload shape#

{
	"event": "requestPaid",
	"data": {
		"id": "TX_UUID",
		"requestId": "REQUEST_UUID",
		"token": "USDC",
		"amount": 100.5,
		"amountCurrencySymbol": "USD",
		"tokenAmount": "100.5",
		"hashUrl": "https://etherscan.io/tx/0x...",
		"network": "Ethereum"
	}
}

Use data.id as the transaction id and data.requestId as the payment request id. Confirm final state with GET /channel/vudy/request/{requestId} or GET /v1/tx/{id}.

curl simulation (local testing)#

curl -sS https://your.app/webhooks/vudy \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Secret: YOUR_DASHBOARD_SECRET" \
  -d '{"event":"requestPaid","data":{"id":"TX_UUID","requestId":"REQUEST_UUID"}}'

Out of scope#

  • Programmatic API-key provisioning that attaches webhook URLs
  • Inbound provider webhooks (platform ingress)
  • Guarantees of automatic retries, ordering, or exactly-once delivery

Next steps#