Receiving a Receipt

Receivers are typically financial institutions or spend management apps. As a receiver, you’ll collect and display your customers' receipts. Receivers display receipts to help customers manage spend, reclaim VAT, comply with audits, etc.

To onboard as a receiver, you’ll need to:

  1. Set up your client
  2. Register your customers
  3. Collect incoming receipts
  4. Render receipts

Client Configuration

First, you’ll need to create an account on Versa.

Just as on the sending end, receivers need to choose how they’d like to receive receipts:

  1. Via a custodial configuration. With this arrangement, you’ll receive webhook events containing the receipt JSON at the URL you specify. Versa takes care of the decryption for you.
  2. Via a self-hosted configuration. With this arrangement, the receiver collects and decrypts receipts directly. You’ll still receive webhook events, but the webhook body will include an encrypted receipt and nonce, and you will be responsible for checking out the key from the Versa Registry and decrypting the receipt.

The custodial configuration is typically easier for developers to implement. The self-hosted configuration, on the other hand, removes the need to trust a third-party (Versa) with the receipt data. At any point you can 'eject' from a custodial to a self-hosted configuration. Most developers start with a custodial configuration during development, then switch to self-hosted.

Customer Registration

Before you can start receiving receipts, you’ll need to register the customers who have authorized you as their receipt receiver. When you register a customer, you add their 'handle' — the identifier that Versa uses to know if and where to send receipts.

You are only able to receive receipts on behalf of customers who have explicitly granted you that permission.

To register a handle via API:

curl --request POST \
  --url 'https://registry.versa.org/customer' \
  --header 'Authorization: Basic CLIENT_ID:CLIENT_SECRET' \
  --header 'Content-Type: application/json' \
  --data '{
    "handle_type": "customer_email_domain",
    "handle": "acme.com"
  }'
FieldTypeDescription
handle_typeenumOne of customer_email_domain (e.g. acme.com) or customer_email (e.g. jshmoe@acme.com).
handlestringThe handle value.

See Customer Registration for full details on keeping your customer roster synced.

Collecting Incoming Receipts

Once your customers are registered with Versa, incoming receipts that match those customer profiles will start hitting the webhook address you’ve specified in your client. The contents of this webhook request body depend on whether you’ve configured a custodial or self-hosted client.

Custodial Configuration

If you’ve set up a custodial client, you’ll receive decrypted receipt objects at your webhook address. You’re free to store and render these receipts within your product.

The payload looks like this:

{
  "event": "receipt.decrypted",
  "event_id": "evt_6abf1062dc2f4844a81b645b9a5dbf41",
  "event_at": 1739315358,
  "delivery_id": "whd_809aba728d6940de8e294af8b3ad272c",
  "delivery_at": 1739315358,
  "data": {
    "receipt": { ... },
    "checkout": {
      "sender": {
        "logo": "https://imagedelivery.net/image_url.png",
        "name": "Supplier Co.",
        "org_id": "org_b6d073a9bf3a4c49a8c03191f87d8ee2",
        "website": "supplier.com",
        "brand_color": "#f0C14B"
      },
      "handles": {
        "customer_email": "john.smith@example.com",
        "customer_email_domain": "example.com"
      },
      "receipt_id": "rct_14a9817fea7d41bf941c16b8750cc845",
      "registered_at": 1739315357,
      "schema_version": "2.1.0",
      "transaction_id": "txn_6b408ba7406b4754b7b7d287e0ca9fdf",
      "transaction_event_index": 0
    }
  }
}

You may also retreive PDFs in a custodial client configuration. Learn more.

Self-Hosted Configuration

If your client is configured in 'self-hosted' mode, you’ll receive encrypted data, and then you’ll manage the decryption directly.

The fastest way to get started with a self-hosted receiver client is to use the official Versa Docker image. This image includes a simple receiver client that you can run on your own infrastructure. You'll need to provide your CLIENT_ID, CLIENT_SECRET, and WEBHOOK_SECRET as environment variables, configure the Dockerized service's address as the webhook target in your Versa client settings, and configure a separate target in your stack to receive the decrypted receipts. The service will automatically handle all the protocol steps (see below). Or, if you prefer, you can implement a client from scratch in your language of choice, as outlined in the Receiver Implementation Guide.

Your responsiblities will then include:

  1. Receive Encrypted Receipt
  2. Get the Key
  3. Decrypt Receipt
  4. Validate Receipt

1. Receive the Encrypted Receipt

An example payload that you will receive at your registered address, from the sending merchant:

{
  "receipt_id": "rct_5ed073abbf3a4b49a8c03191f87d8ffe",
  "envelope": {
    "encrypted": "ozAUXJFhNlJTlfTAi33J4K55HxonNg2CnbAIbUBornUbsS3WG+AAhdbhdFgyB/WmPXTNgFNDRX5CfAqhAnJoDITBN56rncpv",
    "nonce": "okYVc6b9GOeGsaMe"
  }
}

2. Get the Key

Take the receipt_id you received, and exchange it for the matching key, via the Versa Registry.

curl --request POST \
  --url 'https://registry.versa.org/checkout' \
  --header 'Authorization: Basic CLIENT_ID:CLIENT_SECRET' \
  --header 'Content-Type: application/json' \
  --data '{
    "receipt_id": "rct_5ed073abbf3a4b49a8c03191f87d8ffe"
  }'
FieldTypeDescription
receipt_idstringThe id of the specific receipt being received.

Example response:

{
  "key": "jZt54/mk2RjP4qtNekQQyamu4qoEaxCE/qk5fhi5LuQ=",
  "receipt_id": "rct_5ed073abbf3a4b49a8c03191f87d8ffe",
  "transaction_id": "txn_5ed073abbf3a4b49a8c03191f87d8ffe",
  "transaction_event_index": 0,
  "registered_at": 1739221813,
  "handles": {
    "customer_email": "joe@acme.com"
  },
  "sender": {
    "org_id": "org_b6d073a9bf3a4c49a8c03191f87d8ee2",
    "name": "Supplier Co.",
    "brand_color": "#f0C14B",
    "logo": "https://static.platform.co/image_url.png",
    "website": "supplier.com"
  }
}

The sender is a Versa organization. Organizations are vetted and approved before they are able to send receipts across the network.

3. Decrypt the Receipt

Receipts are decrypted with the AES-GCM-SIV 256 bit encryption algorithm, using the registry-provided key and a unique nonce provided by the sender. See the Receiving Client Implementation doc for language-specific examples.

4. Validate and Report Misuse

If decryption fails, or the resulting data is malformed or invalid, this misuse of the protocol should be reported back to the registry. See Misuse Reporting for more information.

Verifying Inbound Receipts

Whether you are using a self-hosted or custodial client, you should verify incoming requests at your receiving endpoint. This is especially true in custodial mode, where you are not completing the handshake yourself by checking out the key from the Versa registry. In either case, use your webhook secret to produce an HMAC verification token from the request body and compare it to the X-Request-Signature header. The token should be generated with the HMAC-SHA1 algorithm, and base64 encoded.

You can review example code in our client implementation doc.

IP Whitelisting

If you are a self-hosted client, it is not recommended that you whitelist IP addresses for inbound requests to your webhooks, as senders in the Versa network may be posting events from any number of regions and IP addresses.

If you are a custodial client, you may whitelist the IP addresses of Versa's servers to ensure that only Versa can send you events. All requests originating from Versa's servers currently come from the following IP address: 104.154.56.158

Rendering Receipts

Once decrypted, you can store and render the receipt as you see fit. You can roll your own renderer, or you can use a Versa frontend library to render the receipt. We currently offer a React @versa/react renderer package.

import { ReceiptDisplay } from "@versa/react";

export const YourReceiptWrapper = ({
  senderProfileReturnedByRegistry,
  decryptedReceipt,
}) => {
  return (
    <ReceiptDisplay
      merchant={senderProfileReturnedByRegistry}
      receipt={decryptedReceipt}
    />
  );
};

Retrieve PDFs (Custodial Client)

If you are using a custodial configuration, you have the option to retrieve PDFs instead of rendering the structured receipt data. All you need is the receipt_id.

curl --request GET \
  --url 'https://custodial.versa.org/pdf/RECEIPT_ID' \
  --header 'Authorization: Basic CLIENT_ID:CLIENT_SECRET'

Testing Your Integration

Once you’ve set up your client, you can test your integration by sending a test receipt object from the Versa web app. Look for your webhook’s 'Send Test Receipt' button at the bottom of your Client page. This allows you to verify that you’re correctly receiving and processing receipts at your webhook address.

If you’ve opted for custodial hosting, you’ll be able to view the test receipt in your Logs for up to 3 days. After 3 days, receipts are purged from the custodial database.

The test receipt is sent by a sandbox merchant maintained by Versa. It will be clearly marked as "[TEST]" in the sender data.