Receiving Client Implementation

Developer documentation for implementing a self-hosted Versa receiver client, using the Versa REST API. We currently maintain example client implementations for Rust 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 receiver client performs:

  1. Receive the encrypted receipt
  2. Get the key
  3. Decrypt the receipt

1. Receive the encrypted receipt

When a sender forwards a receipt to you, they will verify their request with an HMAC verification token using your webhook secret. The first step in handling incoming events is verifying this token.

hmac_verify.rs
View on GitHub
async fn verify_with_secret(
  body: axum::body::Body,
  secret: String,
  token: &str,
) -> (bool, hyper::body::Bytes) {
  let mut mac = hmac::Hmac::<sha1::Sha1>::new_from_slice(&secret.as_bytes()).unwrap();
  let body_bytes = axum::body::to_bytes(body, 512_000_000).await.unwrap();
  mac.update(body_bytes.as_ref());
  let code_bytes = mac.finalize().into_bytes();
  let encoded = BASE64_STANDARD.encode(&code_bytes.to_vec());
  (encoded == token, body_bytes)
}
api_receiver/routes.rs
View on GitHub
pub async fn target(headers: HeaderMap, raw_body: axum::body::Body) -> ()
{
  let request_token = headers.get("X-Request-Signature").unwrap().to_str().unwrap();
  let (verified, body_bytes) =
    crate::hmac_verify::verify_with_secret(raw_body, receiver_secret, request_token).await;
  if !verified {
    return ();
  }
  // ...
}

This ensures you are only receiving events from legitimate Versa senders.

2. Get the key

Once the request signature is verified, you can check out the key from the registry.

r_protocol.rs
View on GitHub
pub async fn checkout_key(
  client_id: &str,
  client_secret: &str,
  receipt_id: String,
) -> () {
  let credential = format!("Basic {}:{}", client_id, client_secret);
  let payload = CheckoutRequest { receipt_id };
  let payload_json = serde_json::to_string(&payload).unwrap();

  reqwest::Client::new()
    .post(format!("{}/checkout", registry_url))
    .header("Accept", "application/json")
    .header("Authorization", credential)
    .header("Content-Type", "application/json")
    .body(payload_json)
    .send()
    .await;
}

The registry will record the receipt as delivered and return the key for decryption.

3. Decrypt the receipt

Using the key from the registry and the nonce provided in the received envelope, decrypt the receipt. Deserialize to an object compatible with the receipt’s schema version.

decryption.rs
View on GitHub
pub fn decrypt_envelope<T>(envelope: Envelope, key: &String) -> T
where
  T: for<'a> Deserialize<'a>,
{
  let encrypted_data = BASE64_STANDARD.decode(envelope.encrypted).unwrap();
  let nonce = BASE64_STANDARD.decode(envelope.nonce).unwrap();
  let key = BASE64_STANDARD.decode(key).unwrap();
  let cipher = Aes256GcmSiv::new(key[..].into());
  let decrypted = match cipher.decrypt(nonce[..].into(), Payload::from(&encrypted_data[..])).unwrap();
  let canonical_json = String::from_utf8(decrypted).unwrap();
  serde_json::from_str::<T>(&canonical_json).unwrap()
}

You can now store the decrypted data, forward it to an internal service, or display it to your customer using our React library.

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 checking out a key.

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 key checkout request body with client metadata below:

{
  "receipt_id": "rct_18ca9414f4084b959045d3f1cf6c973b",
  "client_metadata": {
    "client_string": "good-software-company/1.0.0"
  }
}