Webhooks SDK
Extensions can receive external webhook callbacks (e.g. from third-party services like Chatwoot, Stripe, or GitHub) by implementing the WebhookHandler extension point.
How It Works
The platform routes all incoming webhooks through a single endpoint:
POST /api/webhooks/{extensionKey}The host finds the WebhookHandler registered for that extensionKey and delegates the request to it.
WebhookHandler
java
public interface WebhookHandler extends ExtensionPoint {
/** The extension key this handler belongs to (e.g. "chatwoot-extension"). */
String extensionKey();
/**
* Process an incoming webhook.
*
* @param rawBody Unparsed request bytes — use for HMAC signature validation
* @param parsedPayload Deserialized JSON body for convenience
* @param headers Lowercased header name → value map
* @throws WebhookAuthenticationException if signature validation fails → HTTP 401
*/
void handleWebhook(byte[] rawBody, Map<String, Object> parsedPayload, Map<String, String> headers);
}WebhookAuthenticationException
Throw this if HMAC or API-key validation fails. The platform returns HTTP 401.
java
public class WebhookAuthenticationException extends RuntimeException {
public WebhookAuthenticationException(String message) { ... }
public WebhookAuthenticationException(String message, Throwable cause) { ... }
}Implementing a Webhook Handler
java
import org.pf4j.Extension;
import vn.metaone.sdk.webhook.WebhookHandler;
import vn.metaone.sdk.webhook.WebhookAuthenticationException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Map;
@Extension
public class ChatwootWebhookHandler implements WebhookHandler {
private final ExtensionConfigProvider config;
private final ChatService chatService;
public ChatwootWebhookHandler(ExtensionConfigProvider config, ChatService chatService) {
this.config = config;
this.chatService = chatService;
}
@Override
public String extensionKey() {
return "chatwoot-extension";
}
@Override
public void handleWebhook(byte[] rawBody, Map<String, Object> parsedPayload, Map<String, String> headers) {
// 1. Validate HMAC signature
String signature = headers.get("x-chatwoot-signature");
String secret = config.get("system", "chatwoot-extension", "webhookSecret");
validateSignature(rawBody, signature, secret);
// 2. Route by event type
String eventType = (String) parsedPayload.get("event");
switch (eventType) {
case "conversation_created" -> chatService.onConversationCreated(parsedPayload);
case "message_created" -> chatService.onMessageCreated(parsedPayload);
default -> {} // ignore unknown events
}
}
private void validateSignature(byte[] body, String signature, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
String expected = HexFormat.of().formatHex(mac.doFinal(body));
if (!expected.equals(signature)) {
throw new WebhookAuthenticationException("Invalid HMAC signature");
}
} catch (WebhookAuthenticationException e) {
throw e;
} catch (Exception e) {
throw new WebhookAuthenticationException("Signature validation failed", e);
}
}
}Webhook Endpoint
External services should be configured to POST to:
POST https://your-platform-host/api/webhooks/{extensionKey}For example, for the Chatwoot extension:
POST https://your-platform-host/api/webhooks/chatwoot-extensionSecurity Best Practices
- Always validate signatures. Use the raw
rawBodybytes (not the parsed payload) for HMAC computation. - Use
SECRETconfig fields to store webhook secrets — they are encrypted at rest and masked in the admin UI. - Throw
WebhookAuthenticationException(not a generic exception) on validation failures so the platform returns the correct401status. - Respond quickly. For heavy processing, persist the event and process asynchronously (e.g. via an internal queue or by publishing a
PlatformEvent).
Response Codes
| Outcome | HTTP Status |
|---|---|
| Handler completes without exception | 200 OK |
WebhookAuthenticationException thrown | 401 Unauthorized |
| Any other exception | 500 Internal Server Error |