Requirements in Masnet
Before configuring the receiving system, prepare the destination in Masnet. The steps are the same for every system.
- Open Company › Integrations (the Spanish interface labels it Empresa › Integraciones), go to the Destinations tab and select New destination. If the integration has no authorized data yet, Masnet asks you to authorize it first.
- In Destination address (https) enter the receiver public URL. It must be
https, port 443 or 8443 and a public DNS name: user information in the URL, fragments, IP addresses and internal names are not accepted. A query string is allowed; Masnet treats the whole URL as a credential. - In Format choose Standard JSON (
application/json) or Structured CloudEvents (application/cloudevents+json). The event content is the same; see Webhooks. - Select Create destination. Masnet shows the signing secret (
whsec_…) only once. Store it immediately in the receiver secret store; it cannot be read again. - Select Test destination and then Send test. The integration is activated only after a successful test; then use Activate destination.
Application configuration
- Create a flow whose source is an HTTP Listener with a static path, for example
/masnet/events, and publish it with TLS (https, port 443 or 8443) on a public DNS name. - In the listener, the HTTP body becomes the *payload* and the headers become
attributes.headers(per the connector documentation). Masnet sends the names in lowercase:webhook-id,webhook-timestampandwebhook-signature. - Place no transformation before the verification. If the payload is reserialized, the bytes change and the signature no longer matches.
- Store the signing secret in a secure application property, not in the code.
Signature verification
The signature is v1, followed by base64(HMAC-SHA256) computed with the binary key obtained by base64-decoding what follows whsec_, over the bytes webhook-id + . + webhook-timestamp + . + the exact body. Always verify over the received bytes, before parsing the JSON.
One option is to call a Java class from the flow with the static invocation operation of the Java module, passing the secret, the headers, the payload as bytes, the current time and the tolerance (300 seconds). The following class is the dependency-free version; it returns true or false.
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/** Verifies a Masnet webhook with the JDK only. `rawBody` is the exact bytes received. */
public final class JavaManual {
public static boolean verify(String secret, Map<String, String> headers, byte[] rawBody, long nowSeconds, long tolerance) {
String id = headers.get("webhook-id");
String timestamp = headers.get("webhook-timestamp");
String signatures = headers.get("webhook-signature");
if (id == null || signatures == null || timestamp == null || !timestamp.matches("\\d+")) return false;
if (Math.abs(nowSeconds - Long.parseLong(timestamp)) > tolerance) return false;
try {
byte[] key = Base64.getDecoder().decode(secret.replaceFirst("^whsec_", ""));
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
mac.update((id + "." + timestamp + ".").getBytes(StandardCharsets.UTF_8));
byte[] expected = mac.doFinal(rawBody);
// During a rotation the header carries two signatures separated by a space; any match is valid.
for (String part : signatures.split(" ")) {
String[] pair = part.split(",", 2);
if (pair.length != 2 || !pair[0].equals("v1")) continue;
try {
if (MessageDigest.isEqual(Base64.getDecoder().decode(pair[1]), expected)) return true;
} catch (IllegalArgumentException malformed) {
// ignore a malformed entry and keep looking
}
}
} catch (Exception e) {
return false;
}
return false;
}
}Response
If the verification succeeds, answer 200 (the default status of a successful flow). If it is invalid, configure a 401 status with a short body in the listener Responses tab; Masnet treats it as a retryable failure. Process the event in another flow (for example, through a queue) so as not to exceed 15 seconds.
Testing with the test event
- Deploy the application.
- In Masnet, select Test destination and Send test.
- Review the application log: it must show an event with
Masnet-Event-Type: com.masnet.integration.test.v1and a successful verification.
Operation: duplicates and ordering
- Idempotency.
webhook-ididentifies the event and is kept across retries and redeliveries. Record the identifiers already processed and discard repeats with a 2xx response. - Ordering. Each resource (
subject) carries an increasingsequence. Discard an event whosesequenceis lower than or equal to the last one applied for thatsubject. Events of different resources have no order between them. - Retries. Masnet retries any non-2xx response for up to 72 hours; the full schedule is in Webhooks.
- Secret rotation. When rotating with overlap, the previous secret stays valid for 24 hours and
webhook-signaturecarries twov1,…signatures separated by a space. Accept the event if either one matches.
Troubleshooting
Masnet records the response code of each attempt in the delivery history. These are the effects of each receiver response:
| Receiver response | Effect in Masnet |
|---|---|
| 2xx | Successful delivery. It means receipt, not business processing. |
| 3xx | The redirect is not followed. It counts as a failure and is retried; the history shows the Location without its query string. |
| 400, 401, 403, 404 and other 4xx or 5xx | Retryable failure for up to 72 hours from the start of the delivery cycle. |
| 410 | Masnet disables the destination and cancels pending deliveries. Return 410 only if the destination must stop receiving events. |
| 413 | The event is marked as undeliverable to that destination and is not retried. |
429 or 503 with Retry-After | Retry-After (seconds or HTTP date) is honored up to a maximum of 1 hour. |
| No response within 15 seconds | Timeout (5 s to connect, 15 s in total). It is a retryable failure: answer 2xx and process asynchronously. |