Guide

How to receive webhooks in Quarkus Java receiver + reliable processing

A minimal Quarkus endpoint is easy to add. Production reliability (verification, retries, idempotency, backpressure) is the hard part — and Hooque makes that part simple.

Prefer the “no framework” version? Read receive webhooks in Java.

TL;DR

  • Treat “receive webhooks in Quarkus (Java)” as an ops problem, not just a route handler.
  • Verify the request before parsing/side effects (use a verifySignature(...) stub, then implement provider verification).
  • Return 2xx quickly; move work to a worker/queue to avoid timeouts and retries.
  • Assume retries and design idempotency (dedupe by event id + unique constraints).
  • Log + store raw payloads for replayable debugging.
  • If you need one workflow across many providers, centralize ingest + standardize consumption.

Want the standard-library version and shared pitfalls? Read receive webhooks in Java .

Anti-patterns

  • Doing business logic inline in the Quarkus (Java) request handler.
  • Parsing/transforming the body before verification (breaks signing inputs).
  • Returning 2xx before authenticity is proven.
  • Skipping idempotency (retries become double side effects).

Need deeper implementation details? Start with Webhook API.

Why it's hard in production

Frameworks help you build endpoints. They don’t solve retries, replay attacks, or backpressure by default.

Verify authenticity + stop replays

Use a verifySignature(...) stub here, then implement real verification + replay defense for each provider.

Read the guide

Assume retries (duplicates are optional)

Treat every delivery as at-least-once and make side effects idempotent (DB constraints, dedupe keys).

Read the guide

Don’t do work in the request path

Ack fast, process async. Otherwise timeouts, deploys, and spikes turn into missed webhooks.

Read the guide

Debug with real payloads

Save the exact body + headers so you can replay deterministically after a fix.

Read the guide

Add monitoring + alerts early

Track delivered vs rejected, processing latency, queue depth, and error rates.

Read the guide

Iterate locally without losing events

Tunnels help, but durable capture + replay removes the “my laptop was asleep” problem.

Read the guide

Minimal receiver (Quarkus)

Keep verification as a stub here, then implement provider-specific verification + replay protection in the webhook security guide . For the standard-library version and shared pitfalls, see receive webhooks in Java .

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;

@Path("/webhooks")
public class WebhooksResource {
  private void verifySignature(HttpHeaders headers, byte[] body) {
    // don't compromise on security
    // TODO: implement provider-specific signature verification
  }

  private void processData(byte[] body) {
    // TODO: your business logic (DB writes, external API calls, etc.)
  }

  @POST
  @Consumes(MediaType.WILDCARD)
  public Response post(@Context HttpHeaders headers, byte[] body) {
    verifySignature(headers, body);
    
    // What happens if it fails or times out?
    // Most providers retry -> duplicates unless you designed idempotency.
    processData(body);
    
    // IMPORTANT: ack fast to avoid timeouts and duplicate deliveries.
    return Response.ok("ok").build();
  }
}

Hooque turns any webhook into a reliable queue.

Non-obvious scenario: you can’t expose a port

In real deployments, the hardest part is often “where does this endpoint run?” (NAT, corporate networks, locked-down environments, short-lived preview deployments). Hooque decouples inbound receiving from processing so your Quarkus app doesn’t need to be the public receiver.

The easy path: receive with Hooque + consume forever

Receive once (durably), then process from a queue. Your Quarkus app doesn’t have to be the public receiver.

  • Centralize provider-specific verification and reduce “raw body” pitfalls.
  • Buffer spikes and deployments so you don’t drop deliveries.
  • Use explicit Ack / Nack / Reject to control retries.
  • Replay from the UI after a fix (no guessing what payload was sent).

Want the generic patterns? Read Webhook API and migrate to queue-based processing.

Hooque REST polling loop (runs forever)

Poll the queue forever and handle each event outside the provider’s request path.

// Java 11+ (HttpClient)
// Runs forever: poll /next, ack/nack/reject explicitly.
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.*;
import java.util.regex.*;

public class HooquePoller {
  static class Msg {
    String payload;
    Map<String, Object> meta;
    Msg(String p, Map<String, Object> m) { this.payload = p; this.meta = m; }
  }

  public static void main(String[] args) throws Exception {
    String nextUrl = System.getenv().getOrDefault("HOOQUE_QUEUE_NEXT_URL", "https://app.hooque.io/queues/<consumerId>/next");
    String token = System.getenv().getOrDefault("HOOQUE_TOKEN", "hq_tok_replace_me");
    HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();

    while (true) {
      Msg msg = getNextMessage(client, nextUrl, token);
      if (msg == null) {
        Thread.sleep(1000);
        continue;
      }

      try {
        processData(msg.payload, msg.meta);
        ack(client, msg, token);
      } catch (Exception err) {
        nack(client, msg, token, err);
      }
    }
  }

  private static Msg getNextMessage(HttpClient client, String nextUrl, String token) {
    try {
      HttpRequest req = HttpRequest.newBuilder(URI.create(nextUrl))
        .header("Authorization", "Bearer " + token)
        .timeout(Duration.ofSeconds(30))
        .GET()
        .build();

      HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
      if (resp.statusCode() == 204) return null;
      if (resp.statusCode() >= 400) {
        System.out.println("next() failed: " + resp.statusCode() + " " + resp.body());
        Thread.sleep(2000);
        return null;
      }

      String metaRaw = resp.headers().firstValue("X-Hooque-Meta").orElse("{}");
      Map<String, Object> meta = new HashMap<>();
      meta.put("ackUrl", jsonField(metaRaw, "ackUrl"));
      meta.put("nackUrl", jsonField(metaRaw, "nackUrl"));
      meta.put("rejectUrl", jsonField(metaRaw, "rejectUrl"));
      meta.put("messageId", jsonField(metaRaw, "messageId"));

      return new Msg(resp.body(), meta);
    } catch (Exception e) {
      System.out.println("Worker connection err: " + e.getMessage());
      try { Thread.sleep(2000); } catch (InterruptedException ignore) {}
      return null;
    }
  }

  private static void processData(String payload, Map<String, Object> meta) throws Exception {
    // Example real-life task: run a script on webhook events.
    // new ProcessBuilder("./on_webhook.sh").inheritIO().start().waitFor();
    System.out.println("event: " + meta.get("messageId"));
  }

  private static void ack(HttpClient client, Msg msg, String token) throws Exception {
    String url = (String) msg.meta.get("ackUrl");
    if (url != null) post(client, url, token, null);
  }

  private static void nack(HttpClient client, Msg msg, String token, Exception err) {
    try {
      String nackUrl = (String) msg.meta.get("nackUrl");
      String rejectUrl = (String) msg.meta.get("rejectUrl");
      String url = nackUrl != null ? nackUrl : rejectUrl;
      if (url != null) {
        post(client, url, token, "{\"reason\":\"" + jsonEscape(err.getMessage()) + "\"}");
      }
    } catch (Exception ignored) {}
  }

  private static String jsonField(String json, String key) {
    Pattern p = Pattern.compile("\"" + Pattern.quote(key) + "\"\\s*:\\s*\"([^\"]+)\"");
    Matcher m = p.matcher(json == null ? "" : json);
    return m.find() ? m.group(1) : null;
  }

  private static String jsonEscape(String s) {
    if (s == null) return "";
    return s.replace("\\", "\\\\").replace("\"", "\\\"");
  }

  private static void post(HttpClient client, String url, String token, String jsonBody) throws Exception {
    HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(url))
      .header("Authorization", "Bearer " + token)
      .timeout(Duration.ofSeconds(30));
    if (jsonBody != null) b.header("Content-Type", "application/json");
    HttpRequest req = (jsonBody == null)
      ? b.POST(HttpRequest.BodyPublishers.noBody()).build()
      : b.POST(HttpRequest.BodyPublishers.ofString(jsonBody)).build();
    client.send(req, HttpResponse.BodyHandlers.discarding());
  }
}

Hooque SSE stream consumer (runs forever)

Stream events in real time and reconnect forever on disconnects.

// Java 11+ — SSE consumer (HttpClient)
// Runs forever: connect to /stream, handle "message" events, ack/nack/reject explicitly.
import java.io.*;
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.*;
import java.util.regex.*;
import java.util.function.Consumer;

public class HooqueSse {
  static class Msg {
    String payload;
    Map<String, Object> meta;
    Msg(String p, Map<String, Object> m) { this.payload = p; this.meta = m; }
  }

  public static void main(String[] args) throws Exception {
    String streamUrl = System.getenv().getOrDefault("HOOQUE_QUEUE_STREAM_URL", "https://app.hooque.io/queues/<consumerId>/stream");
    String token = System.getenv().getOrDefault("HOOQUE_TOKEN", "hq_tok_replace_me");
    HttpClient client = HttpClient.newHttpClient();

    while (true) {
      try {
        getMessageStream(client, streamUrl, token, msg -> {
          try {
            processData(msg.payload, msg.meta);
            ack(client, msg, token);
          } catch (Exception err) {
            nack(client, msg, token, err);
          }
        });
      } catch (Exception e) {
        System.out.println("stream error: " + e.getMessage());
        Thread.sleep(2000);
      }
    }
  }

  private static void getMessageStream(HttpClient client, String streamUrl, String token, Consumer<Msg> onMessage) throws Exception {
    HttpRequest req = HttpRequest.newBuilder(URI.create(streamUrl))
      .header("Authorization", "Bearer " + token)
      .header("Accept", "text/event-stream")
      .timeout(Duration.ofSeconds(0))
      .GET()
      .build();

    HttpResponse<InputStream> resp = client.send(req, HttpResponse.BodyHandlers.ofInputStream());
    if (resp.statusCode() >= 400) throw new RuntimeException("stream failed: " + resp.statusCode());

    try (BufferedReader br = new BufferedReader(new InputStreamReader(resp.body()))) {
      String event = null;
      StringBuilder data = new StringBuilder();
      String line;

      while ((line = br.readLine()) != null) {
        if (line.startsWith(":")) continue;
        if (line.isEmpty()) {
          if ("message".equals(event) && data.length() > 0) {
            Map<String, Object> meta = new HashMap<>();
            String rawData = data.toString();
            String metaRaw = jsonFieldRaw(rawData, "meta");
            
            meta.put("ackUrl", jsonField(metaRaw, "ackUrl"));
            meta.put("nackUrl", jsonField(metaRaw, "nackUrl"));
            meta.put("rejectUrl", jsonField(metaRaw, "rejectUrl"));
            meta.put("messageId", jsonField(metaRaw, "messageId"));

            onMessage.accept(new Msg(rawData, meta));
          }
          event = null;
          data.setLength(0);
          continue;
        }
        if (line.startsWith("event:")) event = line.substring(6).trim();
        if (line.startsWith("data:")) {
          if (data.length() > 0) data.append("\n");
          data.append(line.substring(5).trim());
        }
      }
    }
  }

  private static void processData(String payload, Map<String, Object> meta) throws Exception {
    System.out.println("event: " + meta.get("messageId"));
  }

  private static void ack(HttpClient client, Msg msg, String token) {
    try {
      String url = (String) msg.meta.get("ackUrl");
      if (url != null) post(client, url, token, null);
    } catch (Exception e) {}
  }

  private static void nack(HttpClient client, Msg msg, String token, Exception err) {
    try {
      String nackUrl = (String) msg.meta.get("nackUrl");
      String rejectUrl = (String) msg.meta.get("rejectUrl");
      String url = nackUrl != null ? nackUrl : rejectUrl;
      if (url != null) {
        post(client, url, token, "{\"reason\":\"" + jsonEscape(err.getMessage()) + "\"}");
      }
    } catch (Exception e) {}
  }

  private static String jsonFieldRaw(String json, String key) {
    Pattern p = Pattern.compile("\"" + Pattern.quote(key) + "\"\\s*:\\s*(\\{.*?\\})");
    Matcher m = p.matcher(json == null ? "" : json);
    return m.find() ? m.group(1) : "{}";
  }

  private static String jsonField(String json, String key) {
    Pattern p = Pattern.compile("\"" + Pattern.quote(key) + "\"\\s*:\\s*\"([^\"]+)\"");
    Matcher m = p.matcher(json == null ? "" : json);
    return m.find() ? m.group(1) : null;
  }

  private static String jsonEscape(String s) {
    if (s == null) return "";
    return s.replace("\\", "\\\\").replace("\"", "\\\"");
  }

  private static void post(HttpClient client, String url, String token, String jsonBody) throws Exception {
    HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(url))
      .header("Authorization", "Bearer " + token);
    if (jsonBody != null) b.header("Content-Type", "application/json");
    HttpRequest req = (jsonBody == null)
      ? b.POST(HttpRequest.BodyPublishers.noBody()).build()
      : b.POST(HttpRequest.BodyPublishers.ofString(jsonBody)).build();
    client.send(req, HttpResponse.BodyHandlers.discarding());
  }
}

FAQ

Answers tailored to Quarkus, plus shared webhook production guidance.

How do I get the raw request body in Quarkus?

General: Signature verification typically requires the raw body bytes (before JSON parsing). Ensure your middleware stack does not transform the body before verification.

How Hooque helps: With Hooque, provider delivery goes to a managed ingest endpoint. Your worker consumes from a queue using REST or SSE, so the “raw body vs parsed body” pitfall is mostly confined to ingest configuration.

What status code should I return for webhooks in Quarkus (Java)?

General: Usually return a fast 2xx after validating authenticity and basic schema. Timeouts and 5xx commonly trigger retries.

How Hooque helps: Hooque acknowledges ingest immediately and persists the payload. Your worker acks/nacks/rejects explicitly after processing.

Do I need signature verification in Quarkus (Java)?

General: Yes, unless the sender is fully trusted and on a private network. A public endpoint without verification is easy to forge and easy to replay.

How Hooque helps: Hooque can verify at ingest for supported providers or using generic strategies. Either way, your worker receives a normalized meta object and can stay focused on processing.

Why do I see duplicate webhook events in Quarkus (Java)?

General: Retries are normal: timeouts, transient network failures, and 5xx responses all produce duplicates. Design idempotency around event ids and side-effect boundaries.

How Hooque helps: Hooque makes delivery outcomes explicit (ack/nack/reject) and provides replay/inspection so you can fix issues without guessing what was received.

How do I test webhooks locally in Quarkus (Java)?

General: You can use a tunnel, but local dev still breaks on sleep, VPNs, clock skew, and signature-byte mismatches.

How Hooque helps: With Hooque you can avoid inbound locally: receive events into a durable queue and pull/stream to your laptop, then replay from the UI after changes.

Should I use REST polling or SSE streaming for webhook processing?

General: Use REST polling for simple batch workers and environments without long-lived connections. Use SSE for low-latency “process as it arrives” flows.

How Hooque helps: Hooque supports both: `GET /next` for polling and `GET /stream` for streaming. Both include meta with ready-to-call ack/nack/reject URLs.

Start processing webhooks reliably

Use Quarkus for your app, and keep webhook processing as a simple run-forever consumer loop with explicit ack/nack/reject control.

No credit card required