Skip to content

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-extension

Security Best Practices

  1. Always validate signatures. Use the raw rawBody bytes (not the parsed payload) for HMAC computation.
  2. Use SECRET config fields to store webhook secrets — they are encrypted at rest and masked in the admin UI.
  3. Throw WebhookAuthenticationException (not a generic exception) on validation failures so the platform returns the correct 401 status.
  4. Respond quickly. For heavy processing, persist the event and process asynchronously (e.g. via an internal queue or by publishing a PlatformEvent).

Response Codes

OutcomeHTTP Status
Handler completes without exception200 OK
WebhookAuthenticationException thrown401 Unauthorized
Any other exception500 Internal Server Error

MetaOne Platform Documentation