Webhooks v1.1
Malga uses the webhooks service to notify your system about events occurring in our platform. Through webhooks you can update your system whenever an important event happens, such as updating the status of charges to confirm or cancel a certain payment.
Looking for version 1.0? See here
Basic flow to receive notifications via webhooks:
- Create a service with an endpoint accessible within your system to receive event notification requests made by Malga.
- Register your endpoint with Malga by creating a webhook to receive notifications of desired events.
- Malga will send an HTTP request to your endpoint with the modified object data whenever the given event registered in your webhook happens.
Creating a webhook
Create and manage webhooks using the Webhook Service.
curl --location --request POST 'https://api.malga.io/v1/webhooks' \
--header 'X-Client-Id: <YOUR_CLIENT_ID>' \
--header 'X-Api-Key: <YOUR_SECRET_KEY>' \
--header 'Content-Type: application/json' \
--data-raw '{
"event": "transaction.authorized",
"endpoint": "https://enuqkxq2lu8be0y.m.pipedream.net",
"version": 1,
"status": true
}'
< HTTP/2 201
{
"id": "31c142ad-4c30-4964-ba24-2df0f2bbb745",
"clientId": "cc0b1e41-2936-45c5-947f-93995ffcdc00",
"event": "transaction.authorized",
"endpoint": "https://enuqkxq2lu8be0y.m.pipedream.net",
"version": 1,
"status": true,
"createdAt": "2021-07-06T21:03:36.590Z",
"updatedAt": "2021-07-06T21:03:36.590Z"
}
Notification event sent
Many of the events that occur in your Malga integration are synchronous, and you receive a direct response to your request, as in the cases of creating a customer, creating a card, etc.
However, in certain cases, the response you receive after making a request does not include the final status of that object, making it necessary to register a webhook to receive asynchronous responses from Malga's API, keeping your system up to date. This happens mainly in cases of billing through PIX and Invoices/Payment slips, notification of suspected fraud, financial clearance of transactions, among others.
When a certain event occurs, Malga creates an event object which is sent through an HTTP request to your registered endpoint. The event is immutable within the Malga's notification structure, meaning that the data of the object that has changed is saved along with the event, representing the state of the object immediately after the event that changed it.
Example of event notification request sent by Malga to your endpoint:
> POST <ENDPOINT_URL> HTTP/2
> Host: <ENDPOINT_HOST>
> User-Agent: axios/0.21.1
> Accept: application/json, text/plain, */*
> Content-Type: application/json
> x-idempotency-key: 5616b19e-4d99-4bd3-b415-4990e5cab4f4
> HTTP/2
{
"id": "5616b19e-4d99-4bd3-b415-4990e5cab4f4",
"apiVersion": "1",
"object": "transaction",
"event": "authorized",
"createdAt": "2021-07-05T18:56:08.672Z"
"data": {
"id": "242b9be8-cd60-461d-af27-f31e3d6e3fb7",
"updatedAt": "2021-07-05T18:56:08.247Z",
"createdAt": "2021-07-05T18:56:08.247Z",
"idempotencyKey": null,
"requestId": null,
"amount": 1500,
"originalAmount": 1500,
"installments": 1,
"clientId": "cc0b1e41-2936-45c5-947f-93995ffcdc00",
"description": null,
"statementDescriptor": "Pedido #231 loja joão",
"status": "authorized",
"capture": true,
"fee": null,
"feeAmount": null,
"transactionRequests": [{
"id": "87c3973c-6a8d-40a5-8b3b-f6e89a8393ab",
"updatedAt": "2021-07-05T18:56:08.648Z",
"createdAt": "2021-07-05T18:56:08.255Z",
"idempotencyKey": "fdd134fa-f79e-4164-b635-a6510908e1a2",
"requestId": null,
"providerId": "367b118f-a9be-421b-bb61-5df4261df634",
"providerType": "STRIPE",
"transactionId": "ch_3JYE7MHjGFBGEeiP0lfTD3Ob",
"amount": 1500,
"authorizationCode": "486677",
"authorizationNsu": "d8d230ce-7222-4ca6-b08f-135289588bc8",
"responseCode": "1",
"requestStatus": "success",
"requestType": "authorization",
"responseTs": "377ms",
}]
}
}
Best practices to receive notifications
Attempts and reattempts to send notifications
Malga will attempt to send a given notification to your webhook, in case of problems processing the service, we will make new attempts to deliver the notifications escalating the interval between attempts.
To finish processing the notification, your service must return an HTTP STATUS 200 (OK) or 201 (CREATED) within the maximum waiting time, otherwise it will be understood that the endpoint did not receive it correctly and the event will be marked for reattempt.
Event | Interval of the sendings | Response timeout |
---|---|---|
Created | instantly | 30 seconds |
First attempt | 5 seconds | 5 seconds |
Second attempt | 45 seconds | 5 seconds |
Third attempt | 6 hours | 5 seconds |
Fourth attempt | 2 days | 5 seconds |
Fifth attempt | 4 days | 5 seconds |
info
Malga keeps track of all notifications made, as well as the Request and Response information from the external server. After the maximum number of event delivery attempts is exceeded, we mark the message as lost and it is stored for future reprocessing upon request.
The URL of your webhook must be exposed (public) to the internet, so that the Malga platform can reach it and send the events.
Processing events, order and duplicity
When a certain event occurs, Malga then creates an event object which registers the object and the type of the update event as well as the date of occurrence. Each event has a unique identifier that must be used client-side to avoid duplicate processing, the identifier is sent in the event object in the request body and also in the request's x-idempotency-key header, of the same value.
The events are sent through an HTTP request to your endpoint exactly in the order they occurred in the Malga's system, but we recommend that you use the creation date of the event, also sent in the event object, to ensure a chronological order in the processing of client-side events. If you receive an event with a creation date lower than the creation date of another event already processed by your system, the data of the object sent in the event will be outdated, leaving it up to you to take action with this event or not.
Testing notification via webhooks
To test your integration with Malga's webhooks, you can develop your system directly or use a service like request.bin or pipedream.com to primarily validate the events you sent. Just generate a new endpoint in these services and register a webhook on Malga with the generated endpoint and all the events sent will be registered in these services for consultation and debugging.
In sandbox sandbox-api.malga.io
you can manually update created transactions to authorized
, voided
and charged_back
, useful to simulate desired chrage status and test your integration.
Request to update one transaction in sandbox environment
curl --location --request POST 'https://api.malga.io/v1/charges/<CHARGE_ID>' \
--header 'X-Client-Id: <YOUR_CLIENT_ID>' \
--header 'X-Api-Key: <YOUR_SECRET_KEY>' \
--header 'Content-Type: application/json' \
--data-raw '{
"status": "charged_back"
}'
Webhook Security
As of version 1.1 of the webhook, the Malga started to subscribe to all events to ensure the security of receiving the event.
We use a private key of type Ed25519 to sign the events. When registering the webhook, you receive the public key of the same type to verify that the signature sent matches the payload received.
Every event received must be verified and if the signature is not recognized, for security reasons, you must discard the event.
We send 2 headers:
- X-Plug-Date containing the date the event was generated. The format follows the UTC Unix Timestamp standard.
- X-Plug-Signature containing a 64-bit hexadecimal hash.
To prevent a reply attack make sure the event date (X-Plug-Date) is within your acceptance criteria. We recommend not accepting events longer than 5 minutes.
Now you must validate the signature (X-Plug-Signature). For this you will need to use 4 data, X-Plug-Date, X-Plug-Signature, payload and the public key (pubKey) (which is returned when creating from the webhook).
First you must concatenate the date with the body encoded to utf8, encrypt it with the public key of the created webhook, and validate the cryptogram received in the header signature with the generated cryptogram to verify integrity.
The algorithm for verifying the signature looks like the one stated below:
date = header['X-Plug-Date']
msg = "{date}\n{payload}"
sig_raw = header['X-Plug-Signature']
sig_byte = hex_to_bin(sig_raw)
EdDSA_signature_verify(msg, pubKey, sig_byte) --> bool
See more about EdDSA e Ed25519
Samples of how to validate the signature
Examples are available in this github repository
https://github.com/plughacker/plug-sample-signature-verify/
See a snippet of each language
- Node.js
- Golang
- Python
- C#
- Java
- PHP
- Ruby
const crypto = require("crypto");
module.exports = {
/**
* Example of a function that validates the payload signature
* @param {string} publicKey Key returned on webhook creation
* @param {string} payload Event sent by the plug. This information goes in the body http
* @param {number} signatureTime Time in unix timestamp that the event was subscribed to
* @param {string} signature Hash sha512
* @returns {bool}
*/
verify: function (publicKey, payload, signatureTime, signature) {
const payloadUtf8 = Buffer.from(payload, "utf-8").toString();
const signatureBuffer = Buffer.from(signature, "hex");
const data = Buffer.from(`${signatureTime}\n${payloadUtf8}`);
return crypto.verify(null, data, publicKey, signatureBuffer);
},
};
func Verify(publicKeyRaw string, payload string, signatureTime int64, sigHex string) (bool, error) {
sig, err := hex.DecodeString(sigHex)
if err != nil {
return false, err
}
publicKey, err := getEd25519PublicKey(publicKeyRaw)
if err != nil {
return false, err
}
body := fmt.Sprintf("%d\n%s", signatureTime, payload)
message := []byte(body)
isValid := ed25519.Verify(publicKey, message, sig)
return isValid, nil
}
signaturePayload = str.encode(httpSample['headers']['X-Plug-Date'] + "\n" + httpSample['body'])
signature = bytes.fromhex(httpSample['headers']['X-Plug-Signature'])
try:
public_key.verify(signature, signaturePayload)
print('Congrats ... signature is valid!!!')
except:
print('Ops ... Signature is not valid!')
namespace PlugPagamentos;
using System.Text;
using NSec.Cryptography;
public class PlugSignature
{
private static Ed25519 algorithm = SignatureAlgorithm.Ed25519;
public static Boolean verify(String payload, long signatureDate, String signatureHex, string publicKeyRaw)
{
var signature = Convert.FromHexString(signatureHex);
var payloadToVerify = Encoding.UTF8.GetBytes($"{signatureDate}\n{payload}");
var publicKeyBytes = Encoding.UTF8.GetBytes(publicKeyRaw);
var publicKey = PublicKey.Import(algorithm, publicKeyBytes, KeyBlobFormat.PkixPublicKeyText);
return algorithm.Verify(publicKey, payloadToVerify, signature);
}
}
package com.plugpagamentos;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import org.bouncycastle.util.encoders.Hex;
public class PlugSignature {
public static boolean verify(String publicKeyHex, String payload, String signatureTime, String signatureHex) {
var publicKeyBytes = Hex.decode(publicKeyHex);
var signatureBytes = Hex.decode(signatureHex);
var signingDataBytes = (signatureTime+"\n"+payload).getBytes();
var params = new Ed25519PublicKeyParameters(publicKeyBytes, 0);
var verifier = new Ed25519Signer();
verifier.init(false, params);
verifier.update(signingDataBytes, 0, signingDataBytes.length);
return verifier.verifySignature(signatureBytes);
}
}
$data = "{$sampleHttp['headers']['X-Plug-Date']}\n{$sampleHttp['body']}";
// convert signature hex to bin
$signature = hex2bin($sampleHttp['headers']['X-Plug-Signature']);
if ($key->verify($data, $signature)) {
echo 'Congrats ... signature is valid!!!';
} else {
echo 'Ops ... Signature is not valid!';
}
sig_bytes = hex_to_bin(http_sample['headers']['X-Plug-Signature'])
message = http_sample['headers']['X-Plug-Date'] + "\n" + http_sample['body']
begin
verify_key.verify(sig_bytes, message)
puts 'Congrats ... signature is valid!!!'
rescue StandardError
puts 'Ops ... Signature is not valid!'
end
Supported events for notification via webhooks:
Event | Description |
---|---|
transaction.pending | Event sent when the charge is registered and payment data is available |
transaction.pre_authorized | Event sent when the payment confirmation of the charge is recognized |
transaction.authorized | Event sent when the payment confirmation of the charge is recognized |
transaction.failed | Event sent when the charge is denied by the financial institution before it has been authorized |
transaction.canceled | Event sent when the charge is canceled after being authorized but not captured, without financial refund |
transaction.voided | Event sent when the charge is cancelled after it has been authorized and captured, creating a financial refund |
transaction.charged_back | Event sent when the charge is cancelled after being disputed and/or not recognized by the cardholder |
transaction.dispute | Event sent when a transaction related dispute is opened |
transaction.dispute_closed | Event sent when a dispute is closed. If you get charged_back instead of dispute_closed, it means the customer won the dispute. |
transaction.refund_pending | Event sent when a chargeback is pending. This can happen on asynchronous streams like PIX. |