Web3 Donation Widget
Self-host and controll the entire donation flow within your app.
Example
Integration examples
Jump straight into example integrations for common frameworks and platforms:
- Express
- Next.js
Installation
Package
You can install DePay Widgets via yarn
or npm
and build it as part of your application:
- Yarn
- NPM
yarn add @depay/widgets
npm install @depay/widgets --save
Make sure you install DePay widgets peer dependencies, too, in case your project does not have them installed yet:
- Yarn
- NPM
yarn add ethers react react-dom
npm install ethers react react-dom --save
CDN
If you don't want to install the package or don't want to build DePay Widgets as part of your application, you can also load DePay Widgets via CDN:
<script defer async src="https://integrate.depay.com/widgets/v12.js"></script>
Create an integration
Go to https://app.depay.com/dev/integrations and click "New Integration".
Make sure you select the "Donation Widget" integration.
Give your integration a name so that you can identify it later on.
Accepted tokens/blockchains
Choose the tokens you wish to accept as donation methods. Ensure you provide a receiving wallet address for every selected token.
Place integration code
Now you can place the integration code into your app and open the DePay Donation widget:
import DePayWidgets from "@depay/widgets"
DePayWidgets.Payment({
integration: 'YOUR-INTEGRATION-ID'
});
Redirect after donation
Enter the URL to which users should be redirected after a successful donation.
If you need to configure dynamic redirects, continue reading how to setup dynamic configurations.
Configure callbacks
Set up an endpoint to be called upon each successful donation.
The callbacks will execute a POST
request to the specified URL.
Ensure you provide an HTTPS URL.
The callback's request body will be structured as follows:
{
"blockchain": "polygon",
"transaction": "0x053279fcb2f52fd66a9367416910c0bf88ae848dca769231098c4d9e240fcf56",
"sender": "0x317D875cA3B9f8d14f960486C0d1D1913be74e90",
"receiver": "0x08B277154218CCF3380CAE48d630DA13462E3950",
"token": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
"amount": "0.0985",
"payload": null,
"after_block": "46934392",
"commitment": "confirmed",
"confirmations": 1,
"created_at": "2023-08-30T11:37:30.157555Z",
"confirmed_at": "2023-08-30T11:37:35.492041Z"
}
Make sure your callback endpoint responds with 200, as otherwise the widget will not release the user. See payment flow.
Only successful payments are delivered to the configured callback.
Callback requests will retry any uncessfull response (response code was not 200) with an exponential backoff using the formula (retry_count * 4) + 15 + (rand(30) (retry_count + 1)) (i.e. 15, 16, 31, 96, 271, ... seconds + a random amount of time).
It will perform 25 retries over approx. 21 days.
Redirect user
If you want to dynamically redirect users upon callback response, provide the location with forward_to
as part of the callback response:
{
"forward_to": "https://example.com/depay/success/1212391238123"
}
Configure events
If you want your systems to be informed about the different events occuring during the payment flow, configure an events endpoint url for your integration on https://app.depay.com.
Once configured, event requests will execute a POST
request to the specified URL.
Ensure you provide an HTTPS URL.
The event's request body will be structured as follows:
{
"status": "attempt",
"blockchain": "polygon",
"transaction": "0x053279fcb2f52fd66a9367416910c0bf88ae848dca769231098c4d9e240fcf56",
"sender": "0x317D875cA3B9f8d14f960486C0d1D1913be74e90",
"receiver": "0x08B277154218CCF3380CAE48d630DA13462E3950",
"token": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
"amount": "0.0985",
"payload": null,
"after_block": "46934392",
"commitment": "confirmed",
"confirmations": 1,
"created_at": "2023-08-30T11:37:30.157555Z",
"confirmed_at": "2023-08-30T11:37:35.492041Z"
}
status
can be one of attempt
, processing
, failed
or succeeded
.
Event requests will retry any uncessfull response (response code was not 200) with an exponential backoff using the formula (retry_count * 4) + 15 + (rand(30) (retry_count + 1)) (i.e. 15, 16, 31, 96, 271, ... seconds + a random amount of time).
It will perform 25 retries over approx. 21 days.
Verify communication
On your integration page on app.depay.com you will find a dedicated public key. Store and use it in your application to verify all communication from DePay's APIs to your systems is authentic.
DePay's api calls include an x-signature
header with all requests sent to your systems.
Use that x-signature
header together with the stored public key to verify the request is authentic.
DePay employs RSA-PSS with a salt length of 64 and SHA256 to sign request bodies. The signature is then sent base64 safe URL-encoded via the x-signature
header.
- JavaScript
- Java
- Ruby
- Python
- PHP
- Other
Use DePay's verify-js-signature package for JavaScript & Node:
import { verify } from '@depay/js-verify-signature'
let verified = await verify({
signature: req.headers['x-signature'],
data: JSON.stringify(req.body),
publicKey,
});
if(!verified){ throw('Request was not authentic!') }
public static boolean verifySignature(String signature, String requestBody) throws Exception {
// Decode the Base64 signature
byte[] decodedSignature = Base64.getUrlDecoder().decode(signature);
// Convert PEM public key to PublicKey instance
String pemPublicKey = PUBLIC_KEY_STR
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] publicKeyBytes = Base64.getDecoder().decode(pemPublicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
// Initialize Signature with RSA-PSS
Signature rsaPssSignature = Signature.getInstance("RSASSA-PSS");
PSSParameterSpec pssParams = new PSSParameterSpec(
"SHA-256",
"MGF1",
MGF1ParameterSpec.SHA256,
64,
1
);
rsaPssSignature.setParameter(pssParams);
rsaPssSignature.initVerify(publicKey);
rsaPssSignature.update(requestBody.getBytes(StandardCharsets.UTF_8));
// Verify the signature
boolean isVerified = rsaPssSignature.verify(decodedSignature);
return isVerified;
}
public_key = OpenSSL::PKey::RSA.new(STORED_PUBLIC_KEY)
signature_decoded = Base64.urlsafe_decode64(request.headers["X-Signature"])
data = request.raw_post
verified = public_key.verify_pss(
"SHA256",
signature_decoded,
data,
salt_length: :auto,
mgf1_hash: "SHA256"
)
raise 'Request was not authentic' unless verified
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
import base64
# Load the public key
public_key = serialization.load_pem_public_key(PUBLIC_KEY.encode('utf-8'))
# Decode the signature from the headers
signature_decoded = base64.urlsafe_b64decode(request.headers["X-Signature"])
# Get the raw post data (make sure it's not parsed data!)
data = request.body
# Verify the signature
try:
public_key.verify(
signature_decoded,
data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
except InvalidSignature:
raise ValueError('Request was not authentic')
use phpseclib3\Crypt\RSA;
use phpseclib3\Crypt\PublicKeyLoader;
$signature = $request->get_header('x-signature');
$signature = str_replace("_","/", $signature);
$signature = str_replace("-", "+", $signature);
$key = PublicKeyLoader::load($public_key)->withHash('sha256')->withPadding(RSA::SIGNATURE_PSS)->withMGFHash('sha256')->withSaltLength(64);
if( !$key->verify($request->get_body(), base64_decode($signature)) ) {
throw new Exception("Request was not authentic");
}
You can read up on how to verify RSA PSS signatures in other programming languages: here.
Restrict domains
Integrations permit usage and embedding exclusively on websites hosted on specified domains.
If no domain is entered, domain restriction is entirely deactivated.
Once you specify even a single domain, restriction enforcement is activated.
It's essential to list each domain and subdomain you wish to support separately.
For instance: example.com
, www.example.com
, pay.example.com
.
Dynamic configuration
To pass a dynamic configuration to the widget, such as for conveying dynamic prices or for initiating dynamic redirects after successful donations, you'll need to activate dynamic configurations for the specified integration.
After activation, your dynamic configuration - supplied via an API endpoint from your system - must return a valid widget configuration. This configuration should, at a minimum, detail the accepted donations, including blockchains, tokens, amount, and receiver.
Ensure you supply the widget configurations through your designated API endpoint. Do not pass the "accept" parameter directly to the widget during frontend initialization.
Set endpoint
First, you must specify an HTTPS URL endpoint that the integration will call each time someone attempts to make a donation.
Endpoints need to respond a dynamic configuration under 2 seconds or requests will be dropped otherwise and the widget will not load.
Create private/public key
Similarly to how DePay APIs ensure the authenticity of requests to your systems by cryptographically signing request bodies with RSA-PSS, you'll need to employ the same method when implementing dynamic configurations.
To begin signing your dynamic configuration responses, first generate a private key.
Ensure you have OpenSSL installed to generate private keys.
Install OpenSSL
- macOS
- Windows
- Debian/Ubuntu
Generate private key
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
Please ensure you adhere to the highest security standards when working with private keys. Never share or publicly disclose the private key.
Generate public key
openssl rsa -pubout -in private_key.pem -out public_key.pem
Store public key
Now take the content of the public_key.pem
(not the private key!) and store it with your integration on https://app.depay.com.
The public key format looks like:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4PlPK+oM4nQX5TcmnWAE
UMtd5hL8irx1Fbmwtpg4P7aQA1Y7RJ7/JwEMKs4+kJcgSQqqBoil+YgP2WSGtDnp
ar4jIFIPDWY+eWBe3kGqqse+OxyuVMG/k4iMyQG7wB/9l4gY2udi6qciBiSDlNpo
cs7X+zPrnL1jaO9C85yaEBAe4qpRUXhyjZ32DfduDeCP7p2O+cNHXzNwppsWApnE
L8LOX/UkSlSaduJL2pOEv3zcTupOo38fds7V3MmqaxJfMfH9mWMbvVPfEJ2eeEx6
GKnXhyKyW3MH69iEFCrFgAEk/HKI2bAck4DOyh5wVD4bdks0a9cXRWHI747auCeZ
sQIDAQAB
-----END PUBLIC KEY-----
Integrate responses
After setting up an endpoint and registering a public key with the integration, you can begin tailoring your endpoint to return dynamic configurations.
Incoming requests
Incoming requests will have the following headers:
Accept: application/json,application/vnd.api+json
Accept-Charset: utf-8
Content-Type: application/json; charset=utf-8
X-Signature: 0Lt-bOwigLB_tPzWev5Iwe1YeWFWQ1fTi31wolfisWXuSKfuj53MujGfxkDli_A3R4IgFpgfEF6KmU1tDqYn2bId2HiFG6MYf5v25bhLscJnwAlGyVYMVmnxYyuPYsHMTZvZx61LSxC52TavRw4LN5wq9ux4nw4B30rnqCAaYKAZcUgpKgUwsMRToY0D8AwwW2mkkFk5rJKdx0LAnhz0dpGx5b5lc1v7UbcdzvteU8PBzyXcT2hQ-lMo8dTcdFM6tr_xJRrlxEOzeAKB3b2EfOKS_H9AtzICXT-NGc-HvgWKI56NURAheJweKdAvV7AF5atWTjSLnTFAHFl4NkLFsg==
Ensure you verify the incoming x-signature
header to confirm the request's authenticity. How to verify communication.
Basic response
Responses need to be formatted in JSON.
A basic response includes a fundamental widget configuration detailing the list of accepted tokens for the respective donation. In a basic setup, donations are denominated in tokens:
{
"accept": [
{
"blockchain": "ethereum",
"token": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"receiver": "0x4e260bB2b25EC6F3A59B478fCDe5eD5B8D783B02"
}
]
}
This configuration accepts USDT on the Ethereum blockchain sent to 0x4e260bB2b25EC6F3A59B478fCDe5eD5B8D783B02
as donation.
Consult the widget documentation for a deeper understanding of how widget configurations operate.
Sign your response
For secure communication, DePay mandates the use of RSA-PSS to sign your response, specifying a salt length of 64 and utilizing the SHA256 hashing algorithm. Once signed, ensure that you encode the signature in a base64 URL-safe format and transmit it through the x-signature header:
X-Signature: 0Lt-bOwigLB_tPzWev5Iwe1YeWFWQ1fTi31wolfisWXuSKfuj53MujGfxkDli_A3R4IgFpgfEF6KmU1tDqYn2bId2HiFG6MYf5v25bhLscJnwAlGyVYMVmnxYyuPYsHMTZvZx61LSxC52TavRw4LN5wq9ux4nw4B30rnqCAaYKAZcUgpKgUwsMRToY0D8AwwW2mkkFk5rJKdx0LAnhz0dpGx5b5lc1v7UbcdzvteU8PBzyXcT2hQ-lMo8dTcdFM6tr_xJRrlxEOzeAKB3b2EfOKS_H9AtzICXT-NGc-HvgWKI56NURAheJweKdAvV7AF5atWTjSLnTFAHFl4NkLFsg==
Ensure that you sign the response as string format and that the json string does not contain any line-breaks (\n) or unessary whitespace.
- Node
- Ruby / Rails
- Python
const { Buffer } = require("node:buffer");
import crypto from 'node:crypto';
const privateKeyString = process.env.MY_PRIVATE_KEY;
const privateKey = crypto.createPrivateKey(privateKeyString);
const configuration = {
/// your dynamic configuration
}
const dataToSign = JSON.stringify(configuration);
const signature = crypto.sign('sha256', Buffer.from(dataToSign), {
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: 64,
});
const urlSafeBase64Signature = signature.toString('base64')
.replace('+', '-')
.replace('/', '_')
.replace(/=+$/, '');
res.setHeader('x-signature', urlSafeBase64Signature);
return JSON.stringify(configuration) // make sure to return JSON without line-breaks (\n) or unnessary whitespace
private_key = OpenSSL::PKey::RSA.new(ENV['MY_PRIVATE_KEY'])
signature = private_key.sign_pss("SHA256", response.to_json, salt_length: 64, mgf1_hash: "SHA256")
headers['x-signature'] = Base64.urlsafe_encode64(signature)
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
import base64
import json
import os
# Load the private key
private_key_bytes = os.environ['MY_PRIVATE_KEY'].encode('utf-8')
private_key = serialization.load_pem_private_key(private_key_bytes, password=None)
# Sign the response data
response_data = json.dumps(response).encode('utf-8')
signature = private_key.sign(
response_data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=64
),
hashes.SHA256()
)
# Set the signature in the headers
headers['x-signature'] = base64.urlsafe_b64encode(signature).decode('utf-8')
Passthrough payload
If your dynamic configuration depends on data initially provided to the widget (on the frontend) and this data needs to be relayed to your backend for determining the dynamic configuration, pass your payload to the widget during initialization:
DePayWidgets.Payment({
integration: 'YOUR-INTEGRATION-ID',
payload: {
user: '12345'
}
})
By doing so, the payload will be included when calling your configured endpoint. The request body directed towards your configured endpoint will now encompass:
{
"user": "12345"
}
Dynamic user flow/redirect
For scenarios necessitating the redirection of users to dynamic URLs — which can vary per payment event, such as directing users to diverse confirmation screens — utilize the forward_to
parameter within your dynamic configuration response:
{
"forward_to": "https://example.com/depay/success/1212391238123"
}
Finality
DePay employs two distinct confirmation levels for payment validation based on the transaction value and the underlying blockchain's characteristics. Payments below USD $1,000 are designated as "confirmed" after a single block confirmation. In contrast, payments valued at USD $1,000 or above receive the "finalized" status, which necessitates varying block confirmations depending on the specific blockchain in use:
For an in-depth overview, explore the extended validation section.
Payment flow
Successful payment
Failed payment
Only differs to a successful payment in regards of the validation result and everything happening after.
Ultimately instructing the user to retry the payment.