Sending Client Implementation
Developer documentation for implementing a self-hosted Versa sender client, using the Versa REST API. We currently maintain example client implementations for Rust, Java, and NodeJS. Contact us if you’d like assistance with another language.
TIP
Looking for a quick start? You can use our custodial configuration for faster onboarding.
There are three actions a Versa sender client performs:
1. Register the Transaction
Upon completing a sale, you’ll register the transaction with Versa. You’ll pass the relevant customer handles, via a standard POST request, to the '/register' endpoint. If there are matched receivers, Versa will return the URLs indicating where to post the receipt. Review the registration specification for details.
let versa_client =
versa::client::VersaClient::new(client_id, client_secret)
.with_client_string("example-client/1.0.0")
.sending_client("2.1.0".into());
let transaction_id: Option<String> = None;
versa_client.register_receipt(handles, None).await;
If there are no matches, the registry will return an empty list of receivers. If there are authorized receivers, continue to step 2: encrypting the data for each receiver.
2. Encrypt the Receipt
Versa uses AES-GCM-SIV for the symmetric encryption of receipts. The receiver must check out the key from the registry, confirming that they have up-to-date credentials and authorization for the delivery. The 32-byte key is provided by the registry. Generate a 12-byte nonce for each receiver and send the nonce with the encrypted data to each receiver.
pub fn encrypt_envelope<T>(data: &T, key: &Vec<u8>) -> Envelope
where
T: Serialize,
{
let serde_json = json!(data);
let nonce_bytes = generate_nonce();
let nonce = Nonce::from_slice(&nonce_bytes); // unique to each receiver and included in message
let cipher = Aes256GcmSiv::new(key[..].into());
let encrypted = match cipher.encrypt(nonce, canonicalized.as_bytes()) {
Ok(ciphertext) => BASE64_STANDARD.encode(ciphertext),
Err(e) => panic!("Error encrypting data: {}", e),
};
Envelope {
encrypted,
nonce: BASE64_STANDARD.encode(nonce_bytes),
}
}
Once you have an envelope with the encrypted data and nonce, you’re ready to send a payload to the receiver.
3. Send the Receipt
In this final step, the client makes a POST request to each receiver, with an HMAC verification token generated using their webhook secret (provided by the registry). Loop through the list of receivers returned by the registry, generate a nonce, encrypt and send the data. Review the sending specification for details.
async fn generate_token(body: bytes::Bytes, secret: String) -> String {
let mut mac = hmac::Hmac::<sha1::Sha1>::new_from_slice(&secret.as_bytes()).unwrap();
mac.update(body.as_ref());
let code_bytes = mac.finalize().into_bytes();
let encoded = BASE64_STANDARD.encode(&code_bytes.to_vec());
encoded
}
pub fn send_encrypted_payload(
receiver: &Receiver,
sender_client_id: String,
receipt_id: String,
envelope: Envelope
) -> () {
let payload = ReceiverPayload {
sender_client_id,
receipt_id,
envelope,
};
let event = WebhookEvent<ReceiverPayload> {
data: payload,
event_id: None,
event: Event::Receipt,
};
let payload_json = serde_json::to_string(&event).unwrap();
let byte_body = bytes::Bytes::from(payload_json.clone());
let token = generate_token(byte_body, receiver.secret.clone()).await;
let client = reqwest::Client::new();
let response_result = client
.post(&receiver.address)
.header("Content-Type", "application/json")
.header("X-Request-Signature", token)
.body(payload_json)
.send()
.await;
}
Congratulations! You’ve successfully sent a receipt to an authorized receiver on the Versa network. The receiver will complete the process by checking out the key from the registry and decrypting the data on behalf of your customer.
Customer Registration (Optional)
In certain cases, senders may wish to proactively push their receipts to specific receivers. See the Customer Registration overview and the section on sender registration for details.
let customer_reference = CustomerReference {
handle,
handle_type,
receiver_org_id,
};
versa_client.register_customer_reference(customer_reference).await
Client Metadata
If you've implemented your own self-hosted client
(rather than using the official Versa Docker image)
it's a recommended best practice to identify your implementation version.
To do this, set the client_string field in the optional client_metadata object
when registering a transaction.
A client string should be formatted as {CLIENT_NAME}/{CLIENT_VERSION},
where client_name should be a short, lowercase, human-readable business name like 'good-software-company'
and client_version should identify the build of your software,
such as '1.0.0' or '2.3.4-beta' if you use semver, or else a datetime or timestamp.
A good client_string should be automatically configured to represent the latest, sequential build, e.g. good-software-company/1.2.3 or good-software-company/2025-10-01T12:00:00Z.
This provides useful information for quality assurance and incident management.
See an example of a complete receipt registration request body with client metadata below:
{
"schema_version": "2.1.0",
"handles": {
"customer_email_domain": "acme.com",
"merchant_group_code": "ABC789"
},
"transaction_id": "1234567890abcdef",
"client_metadata": {
"client_string": "good-software-company/1.0.0"
}
}