Webhooks
Understanding the mTLS pattern
By Central Bank regulation, it will be necessary to insert an Efí public key into your server so that communication follows the mTLS standard. In the domain representing your server, you should configure the requirement of the public key (mTLS) that we are providing, so that mutual authentication occurs.
Efí will make 2 requests to your domain (server):
- First Request: We will ensure that your server is requiring an Efí public key. To do this, we will send a request without a certificate and your server should not accept the request. If your server responds with refusal, we will send the 2nd request.
- Second Request: Your server, which must contain the provided public key, should perform the "Hand-Shake" so that communication is established.
Your server must have a minimum TLS version of 1.2.
On your server, configure a 'POST' route with a default response as a string "200". Include our production or Sandbox certificate on your server, below are some examples.
It is recommended that you have a dedicated server to be able to perform webhook configurations, as you need access to some files to make the configurations, as in the examples below.
Examples of server settings
To configure your server, you will need the Efí public keys. Below are the addresses of the keys for the Production and Sandbox environments. These keys must be downloaded and placed on your server.
Attribute | Public Key URL |
---|---|
Production | https://certificados.efipay.com.br/webhooks/certificate-chain-prod.crt |
Sandbox | https://certificados.efipay.com.br/webhooks/certificate-chain-homolog.crt |
The code snippets below aim to exemplify the necessary configurations on your server to perform the hand-shake with our servers.
- Python
- Nginx
- Node
- Apache
- PHP
from flask import Flask, jsonify, request
import ssl
import json
app = Flask(__name__)
@app.route("/", methods=["POST"])
def imprimir():
response = {"status": 200}
return jsonify(response)
@app.route("/pix", methods=["POST"])
def imprimirPix():
imprime = print(request.json)
data = request.json
with open('data.txt', 'a') as outfile:
outfile.write("\n")
json.dump(data, outfile)
return jsonify(imprime)
if __name__ == "__main__":
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations('caminho-certificados/certificado-público.crt')
context.load_cert_chain(
'caminho-certificados/server_ssl.crt.pem',
'caminho-certificados/server_ssl.key.pem')
app.run(ssl_context=context, host='0.0.0.0')
#Developed by the Technical Consulting Team at Efí
server {
#
# ...
#
listen [::]:443 ssl ipv6only=on;
listen 443 ssl;
ssl_certificate server_ssl.crt.pem;
ssl_certificate_key server_ssl.key.pem;
ssl_client_certificate /root/chain-pix-webhooks-prod.crt;
ssl_verify_client optional;
ssl_verify_depth 3;
#
# ...
#
location /webhook {
if ($ssl_client_verify != SUCCESS) {
return 403;
}
proxy_pass /webhook;
}
}
#Developed by the Technical Consulting Team at Efí
const express = require("express");
const fs = require("fs");
const https = require("https");
var logger = require('morgan');
const httpsOptions = {
cert: fs.readFileSync(""), // Domain fullchain certificate
key: fs.readFileSync("/"), // Domain private key
ca: fs.readFileSync(""), // Efí's public certificate
minVersion: "TLSv1.2",
requestCert: true,
rejectUnauthorized: true, // If you need other endpoints not to reject requests without mTLS, you can change it to false
};
const app = express();
const httpsServer = https.createServer(httpsOptions, app);
const PORT = 443;
app.use(logger('dev')); // Comment this line if you don't want the server log to be displayed in your console
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// Endpoint for webhook configuration, you need to register https://SEUDOMINIO.com/webhook
app.post("/webhook", (request, response) => {
// Checks whether the request that arrived at this endpoint was authorized
if (request.socket.authorized) {
response.status(200).end();
} else {
response.status(401).end();
}
});
// Endpoint for receiving the webhook handling /pix
app.post("/webhook/pix", (request, response) => {
if (request.socket.authorized){
//Your code handling the callback
/* EXEMPLO:
var body = request.body;
filePath = __dirname + "/data.json";
fs.appendFile(filePath, JSON.stringify(body) + "\n", function (err) {
if (err) {
console.log(err);
} else {
response.status(200).end();
}
})*/
response.status(200).end();
}else{
response.status(401).end();
}
});
httpsServer.listen(PORT, () =>
console.log(`Express server currently running on port ${PORT}`)
);
//Developed by the Technical Consulting Team at Efí
# ********************************************************************************* #
# Use the first example if you want to require the certificate for authentication #
# mutual on any route in the domain indicated in VirtualHost. #
# Works well for sub-domains. Example: https://www.webhook.your_domain.com.br #
# ********************************************************************************* #
<IfModule mod_ssl.c>
<VirtualHost *:443> # Porta HTTPS
#
# ...
#
SSLCertificateFile /certificate_path/fullchain_ssl.pem #fullchain associated with your domain SSL certificate
SSLCertificateKeyFile /path_certificate/privkey_ssl.pem #privkey associated with your domain SSL certificate
#Efí public key
SSLCACertificateFile /path_certificate/chain-pix-prod.crt
# mTLS Efí
SSLVerifyClient require
SSLVerifyDepth 3
# Treating /pix, always redirecting requests to /webhook
Alias "/pix/" "/var/www/webhook/index.php"
Alias "/pix" "/var/www/webhook/index.php"
#
# ...
#
</VirtualHost>
</IfModule>
# ******************************************************************************** #
# Use the second example, if you want to require the certificate for authentication #
# mutual in only one route from the domain indicated in VirtualHost. #
# Example: https://www.your_domain.com.br/webhook/ #
# ******************************************************************************** #
<IfModule mod_ssl.c>
<VirtualHost *:443> # Porta HTTPS
#
# ...
#
SSLCertificateFile /certificate_path/fullchain_ssl.pem #fullchain associated with your domain SSL certificate
SSLCertificateKeyFile /path_certificate/privkey_ssl.pem #privkey associated with your domain SSL certificate
#Efí public key
SSLCACertificateFile /path_certificate/chain-pix-prod.crt
# mTLS Efí
SSLVerifyClient none
SSLProtocol TLSv1.2
<Location "/webhook">
SSLVerifyClient require
SSLVerifyDepth 3
</Location>
# Treating /pix, always redirecting requests to /webhook
Alias "/webhook/pix/" "/var/www/webhook/index.php"
Alias "/webhook/pix" "/var/www/webhook/index.php"
#
#...
#
</VirtualHost>
</IfModule>
# ********************************************************************************** #
# For this example to work, your server must have configured #
# the mTLS certificate, with the direction to this file, and also with the #
# dealing with /pix. Just as it is done in our example of Apache servers. #
# ********************************************************************************** #
<?php
function resposta($status, $mensagem, $dados)
{
$resposta['status'] = $status;
$resposta['mensagem'] = $mensagem;
$resposta['dados'] = $dados;
$json_resposta = '<pre>' . json_encode($resposta, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . '</pre>';
header("HTTP/1.1 " . $status);
echo $json_resposta;
}
function salvar($dados)
{
// Create a .json file to save the information
$nomeArquivo = './dados.json';
$dadosGravados = json_decode(file_get_contents($nomeArquivo), true);
$arquivo = fopen($nomeArquivo, 'w');
// Enhances the information sent with what was already recorded
array_push($dadosGravados, $dados);
if (fwrite($arquivo, json_encode($dadosGravados))) {
resposta(200, "Request completed successfully!", $dados);
} else {
resposta(300, "Failed to save request data.", $dados);
}
fclose($arquivo);
}
function requisicao($metodo, $body, $parametros)
{
switch ($metodo) {
case 'POST':
salvar($body);
break;
case 'GET':
resposta(200, "Request completed successfully!", $body);
break;
}
}
// Gets the HTTP method, body and parameters of the request
$metodo = $_SERVER['REQUEST_METHOD'];
$parametros = explode('/', trim($_SERVER['REQUEST_URI'], '/'));
$body = json_decode(file_get_contents('php://input'), true);
try {
requisicao($metodo, $body, $parametros);
} catch (Exception $e) {
resposta(400, $e->getMessage(), $e);
}
To have a valid SSL, you must contact a Certificate Authority and generate the private key server_ssl.key.pem
and a public one server_ssl.crt.pem
, thus validating the integrity of the connection. You can do this for free using a utility like Certbot, for example.
Skip-mTLS
For hosting on shared servers, there may be restrictions regarding the insertion of certificates generated by another entity, such as our CA, for example. Therefore, we provide the option to skip mTLS, which allows registering the webhook without the need for mTLS validation.
It is important to note that we will always send the certificate in webhooks, whether in registration or in Pix notification. However, when skip-mTLS is used, you, the integrating party, are responsible for validating our certificate.
If you choose to use the skip mTLS attribute, that is, without mTLS validation on your server, you must implement measures to ensure that the sender of the webhooks to your server is indeed Efí.
We suggest the following two validation methods, but strongly recommend that you use them together:
- Check the communication IP: You can restrict communication to the webhook domain to only accept messages from the IP used by Efí.
- Add a hash to the registered webhook URL: Create an hmac (a proprietary identifier) that will be appended to the end of the URL when registering the webhook. This hash will be used to validate the origin of the notification. Thus, all webhooks sent to your server will have this final identification and your application must validate its presence.
Example:
Original notification URL:https://your_domain.com.br/webhook
How it should be registered with the hash:https://your_domain.com.br/webhook?hmac=xyz&ignorar=
. The termignorar=
will serve to handle the addition of/pix
at the end of the URL.
IP currently used in our communications: '34.193.116.226'.
How to register skip-mTLS:
To configure the Pix webhook, you must use the specific endpoint and pass the parameter x-skip-mtls-checking
in the request header with the value true
or false
depending on whether you want to enable or disable this feature.
The image below shows how this parameter should be provided:
Setting Pix Webhook
Endpoint for configuring the notification service for received Pix. Pix originating from static collections will only be notified if they are associated with a txid
.
A webhook URL can be associated with multiple Pix keys.
On the other hand, a Pix key can only be linked to a single webhook URL.
When registering your webhook, we will send a test notification to the registered URL, however when a notification is actually sent, the path /pix
will be added to the end of the registered URL. To avoid needing two distinct routes, you can add a ?ignorar=
parameter to the end of the registered URL, so that /pix
is not added to your URL route.
PUT /v2/webhook/:chave
webhook.write
Request
- Example 1
- Example 2
{
"webhookUrl": "https://exemplo-pix/webhook"
}
{
"webhookUrl": "https://exemplo-pix/webhook?ignorar="
}
Response
The Responses below represent Success(201) and consumption failures/errors.
- 🟢 201
- 🔴 400
Webhook for notifications about Pix received associated with a txid.
InvalidValueError
{
"nome": "valor_invalido",
"mensagem": "URL inválida"
}
Or
{
"nome": "valor_invalido",
"mensagem": "A URL do webhook deve usar o protocolo HTTPS"
}
Or
{
"nome": "webhook_invalido",
"mensagem": "A autenticação de TLS mútuo não está configurada na URL informada"
}
Or
{
"nome": "webhook_invalido",
"mensagem": "A URL informada está inacessível"
}
Or
{
"nome": "webhook_invalido",
"mensagem": "A URL informada atingiu o tempo limite de resposta"
}
Or
{
"nome": "webhook_invalido",
"mensagem": "A requisição na URL informada falhou com o erro: {{errno}}" //{{errno}} represents a Linux error code: https://man7.org/linux/man-pages/man3/errno.3.html Ex: ECONNRESET, EPIPE
}
Or
{
"nome": "webhook_invalido",
"mensagem": "A URL informada respondeu com o código HTTP {{httpStatus}}" // {{httpStatus}} representa o status HTTP que a url respondeu. Ex: 400, 403, 500.
}
Or
{
"nome": "webhook_invalido",
"mensagem": "Não foi possível receber uma resposta da URL informada"
}
Get webhook information
Endpoint for retrieving information about the pix webhook .
GET /v2/webhook/:chave
webhook.read
Response
The responses below represent Consumption Success(200).
- 🟢 200
{
"webhookUrl": "https://gn-pix-webhook.gerencianet.com.br/webhook/",
"chave": "40a0932d-1918-4eee-845d-35a2da1690dc",
"criacao": "2020-11-11T10:15:00.358Z"
}
Get list of webhooks
Endpoint to retrieve webhooks associated with keys through parameters such as inicio
and fim
. Attributes are entered as query params.
GET /v2/webhook
webhook.read
Request
The snippet below shows how theinicio
and fim
parameters(mandatory) should be passed in the request./v2/webhook/?inicio=2020-10-22T16:01:35Z&fim=2020-10-23T16:01:35Z
Response
The responses below represent Success(200) and consumption failures/errors.
- 🟢 200
- 🔴 400
{
"parametros": {
"inicio": "2021-01-22T16:01:35.000Z",
"fim": "2022-12-30T16:01:35.000Z",
"paginacao": {
"paginaAtual": 0,
"itensPorPagina": 100,
"quantidadeDePaginas": 1,
"quantidadeTotalDeItens": 5
}
},
"webhooks": [
{
"webhookUrl": "https://seudominio.com.br/gn/webhook/",
"chave": "40a0932d-1618-4eee-845d-35a2da1590dc",
"criacao": "2021-05-05T19:52:13.000Z"
},
{
"webhookUrl": "https://.projetosseudominio.seudominio.com.br/gn/webhook/",
"chave": "40a0932d-1918-0eee-845d-35a2da1690dc",
"criacao": "2021-10-18T14:42:41.000Z"
},
{
"webhookUrl": "https://seudominio.com.br/webhook/?ignorar=",
"chave": "40a0032d-1918-45ee-845d-3562da1690dc",
"criacao": "2021-11-03T12:25:15.000Z"
}
]
}
{
"nome": "valor_invalido",
"mensagem": "Campo de data fim deve ser maior ou igual ao campo de data inicio"
}
Cancel Pix webhook
Endpoint for canceling the pix webhook.
DELETE /v2/webhook/:chave
webhook.write
Response
The response below represents Success (204) of consumption.
- 🟢 204
Webhook for Pix notifications has been canceled.
Receiving Callbacks
This service is protected by a layer of mTLS authentication. Callbacks are sent by Efí via POST url-registered-webhook/pix
when there is a change in the Pix status.
To test Pix Cob and Pix CobV charge endpoints in the sandbox environment, you can simulate all status changes returned by our API and webhook.
Charges with values between R$ 0.01 and R$ 10.00 are confirmed, and you will receive the information via webhook.
Charges with values above R$ 10.00 remain active, without confirmation, and there are no webhooks for these cases.
Request
When there is a change in the status of a Pix transaction associated with the registered key, Efí sends a POST
request to the webhook URL you defined. A JSON object (like the examples below) will be sent to your server. Each callback request has a timeout of 60 seconds, meaning it is interrupted if there is no response within 60 seconds.
Examples:
- Received
- Refund
- Send
// Pix received
{
"pix": [
{
"endToEndId": "E1803615022211340s08793XPJ",
"txid": "fc9a43k6ff384ryP5f41719",
"chave": "2c3c7441-b91e-4982-3c25-6105581e18ae",
"valor": "0.01",
"horario": "2020-12-21T13:40:34.000Z",
"infoPagador": "pagando o pix"
}
]
}
// Pix received with payer data
{
"pix": [
{
"endToEndId": "E1803615022211340s08793XPJ",
"txid": "fc9a43k6ff384ryP5f41719",
"chave": "2c3c7441-b91e-4982-3c25-6105581e18ae",
"valor": "0.01",
"horario": "2020-12-21T13:40:34.000Z",
"infoPagador": "pagando o pix",
"gnExtras": {
"pagador": {
"nome": "Consultoria Efi",
"cnpj": "09089356000118",
"codigoBanco":"00416968"
},
}
]
}
// Pix received with Split
{
"pix": [
{
"endToEndId": "E1803615022211340s08793XPJ",
"txid": "fc9a43k6ff384ryP5f41719",
"chave": "2c3c7441-b91e-4982-3c25-6105581e18ae",
"valor": "0.01",
"horario": "2020-12-21T13:40:34.000Z",
"infoPagador": "Teste",
"gnExtras": {
"split": {
"id": "f659e882b00440ef9f07538fb697a6b2",
"revisao": 0
}
}
}
]
}
// Pix received with informed rate
{
"pix": [
{
"endToEndId": "E1803615022211340s08793XPJ",
"txid": "fc9a43k6ff384ryP5f41719",
"chave": "2c3c7441-b91e-4982-3c25-6105581e18ae",
"valor": "0.10",
"horario": "2020-12-21T13:40:34.000Z",
"infoPagador": "pagando o pix",
"gnExtras": {
"tarifa": "0.01"
}
}
]
}
// Refund sent
{
"pix": [
{
"endToEndId": "E12345678202009091221syhgfgufg",
"txid": "c3e0e7a4e7f1469a9f782d3d4999343c",
"chave": "2c3c7441-b91e-4982-3c25-6105581e18ae",
"valor": "110.00",
"horario": "2020-09-09T20:15:00.358Z",
"infoPagador": "0123456789",
"devolucoes":[
{
"id": "123ABC",
"rtrId": "D12345678202009091221abcdf098765",
"valor": "110.00",
"horario": {
"solicitacao": "2020-09-09T20:15:00.358Z"
},
"status": "DEVOLVIDO"
}
]
}
]
}
// Refund rejected
{
...
"devolucoes": [
{
...,
"status": "NAO_REALIZADO",
"motivo": "Saldo insuficiente para realizar a devolução."
}
]
...
}
// Sent Pix
{
"pix": [
{
"endToEndId": "E090893562021030PIf25a7868",
"chave": "2c3c7441-b91e-4982-3c25-6105581e18ae",
"tipo": "SOLICITACAO",
"status": "REALIZADO",
"valor": "0.01",
"horario": "2021-03-04T20:39:47.000Z",
"gnExtras": {
"idEnvio": "123ABC"
}
}
]
}
// Rejected Pix
{
"pix": [
{
"endToEndId": "E090893562021030PIf25a7868",
"chave": "2c3c7441-b91e-4982-3c25-6105581e18ae",
"tipo": "SOLICITACAO",
"status": "NAO_REALIZADO",
"valor": "0.01",
"horario": null,
"infoPagador": "0123456789",
"gnExtras": {
"idEnvio": "123ABC",
"erro": {
"codigo": "AC03",
"origem": "PSP do usuário recebedor",
"motivo": "Número da agência e/ou conta transacional do usuário recebedor inexistente ou inválido"
}
}
}
]
}
Callback requests wait for a response with HTTP status 2XX. If the client's server returns a different status, Efí will make up to 10 new notification attempts. The first new attempt will be made 5 minutes after the failure to send the callback. If the error persists, subsequent attempts will be sent at increasingly longer intervals, as shown in the table below.
In cases where the client's server returns HTTP status 429 (too many requests), Efí's servers will also attempt to send the notification up to 10 times according to the table below.
N° of attempts | Time (in minutes) |
---|---|
1 | 5 |
2 | 10 |
3 | 20 |
4 | 40 |
5 | 80 |
6 | 160 |
7 | 320 |
8 | 640 |
9 | 1280 |
10 | 52560 |