// Minimal webhook receiver for Finzep - Java 11+, no dependencies. Save as WebhookReceiver.java next to FzpClient.java. // Specification: payout-api-guide.html (section "Webhooks") // // javac FzpClient.java WebhookReceiver.java && java WebhookReceiver (set PORT, CLIENT_PRIVATE_KEY, SERVER_PUBLIC_KEY in the environment) // // Verifies OUR signature, decrypts the event with YOUR private key, ignores an event it has already handled (events can arrive more // than once) and answers HTTP 200 quickly. Do the real work in handle(). This file uses a bare ServerSocket only to stay dependency // free - in your application put the same few lines into your own web framework's handler (Spring, Jakarta servlet ...) behind https. import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.PrivateKey; import java.security.PublicKey; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class WebhookReceiver { static final Set handled = ConcurrentHashMap.newKeySet(); // use your database in production: eventId is unique static void handle(String eventJson) { // event: payout.success | payout.failed | refund.accepted | refund.rejected | webhook.test System.out.println("webhook " + eventJson); } public static void main(String[] args) throws Exception { PrivateKey clientPrivate = FzpClient.privateKey(Files.readString(Path.of(System.getenv().getOrDefault("CLIENT_PRIVATE_KEY", "client-private.pem")))); PublicKey serverPublic = FzpClient.publicKey(Files.readString(Path.of(System.getenv().getOrDefault("SERVER_PUBLIC_KEY", "server-public.pem")))); int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8080")); try (ServerSocket server = new ServerSocket(port)) { System.out.println("listening on " + port); while (true) { Socket socket = server.accept(); new Thread(() -> serve(socket, clientPrivate, serverPublic)).start(); } } } static void serve(Socket socket, PrivateKey clientPrivate, PublicKey serverPublic) { try (socket) { InputStream in = socket.getInputStream(); readLine(in); // request line: POST /path HTTP/1.1 Map headers = new HashMap<>(); for (String line = readLine(in); line != null && !line.isEmpty(); line = readLine(in)) { int colon = line.indexOf(':'); if (colon > 0) headers.put(line.substring(0, colon).trim().toLowerCase(), line.substring(colon + 1).trim()); } byte[] body = in.readNBytes(Integer.parseInt(headers.getOrDefault("content-length", "0"))); int status = 200; String answer = "ok"; try { String json = FzpClient.openWebhook(new String(body, StandardCharsets.UTF_8), headers.get("x-webhook-id"), clientPrivate, serverPublic); String eventId = FzpClient.field(json, "eventId"); if (handled.add(eventId)) handle(json); } catch (Exception e) { System.err.println("rejected webhook: " + e.getMessage()); status = 400; // we will retry later answer = "invalid"; } byte[] out = answer.getBytes(StandardCharsets.UTF_8); String head = "HTTP/1.1 " + status + (status == 200 ? " OK" : " Bad Request") + "\r\nContent-Length: " + out.length + "\r\nConnection: close\r\n\r\n"; socket.getOutputStream().write(head.getBytes(StandardCharsets.UTF_8)); // any 2xx tells us it arrived socket.getOutputStream().write(out); socket.getOutputStream().flush(); } catch (Exception e) { System.err.println("connection error: " + e.getMessage()); } } static String readLine(InputStream in) throws java.io.IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); for (int b = in.read(); b != -1 && b != '\n'; b = in.read()) if (b != '\r') buf.write(b); return buf.toString(StandardCharsets.UTF_8); } }