From Indrajeet Sengupta: Example to implement e-invoicing with Revenue Cloud
So for the sake of clarity, we will use an example for Vertex Netherlands to comply with E-Invoicing
Let’s prepare the org.
The first thing that we do is create fields on the Invoice level to get the org ready.
Help article showing how to create custom fields in Salesforce: https://help.salesforce.com/s/articleView?language=en_US&id=platform.adding_fields.htm&type=5
Here are the fields that we will create on the Invoice Object:
- E-Invoicing Status:
- Pending: The invoice has been created but is not yet ready for submission.(Draft Status of Invoice)
- Canceled: DRAFT invoice not used, so canceled it.
- Ready for E-Invoicing: The invoice has been finalized and is ready to be sent to the network. This status will be our trigger.
- Sent to Network: The middleware has successfully sent the invoice data.
- Acknowledged by Network: The network has received the invoice and confirmed its validity.
- Approved: The invoice has been approved by the tax authority (for clearance models).
- Rejected: The invoice was rejected by the network due to errors.
- Correction Needed: Data needs correction.
- Correction Addressed: Correction has been made and can be resent for Approval.
- Error: The integration process failed.
- Exempt: Invoice is exempted from E-Invoicing Process.
- Voided invoice
- Document Number
- Idempotency Key
- Document ID
- Issue Date
- Transmission DateTime
Assuming these fields to be present:
- VAT ID: Account Object
- PEEPOL ID: Account Object
- Company ID: Account Object
Now that we have the relevant fields we want to capture for Netherlands, we will move to the next step
Create Invoice Event
This event is a lightweight message containing key identifiers, like the InvoiceId and AccountId.
- Go to Setup > Integrations > Platform Events.
- Click New Platform Event.
- Name it something clear, like Invoice Posted Event.
- Add a custom field to carry the necessary information. The most important one is Invoice ID (Text). You can also add Account ID.
Create Trigger for the Event
- Go to Setup > Process Automation > Flows.
- Click New Flow and select Record-Triggered Flow.
- Configure the Trigger:
- Object: Invoice
- Trigger the Flow When: A record is updated
- Set Entry Conditions:
- Status Is Changed True
- Status Equals Posted
- Optimize the Flow for: Actions and Related Records
Upon firing, the trigger doesn’t call the middleware directly. Instead, it publishes a Custom Platform Event within Salesforce which can be read by the middleware or Mulesoft. This decouples Salesforce from the middleware, making the system more robust and scalable.
- Click the + icon on the path.
- Select the Create Records element.
- Configure the Element:
- How Many Records to Create: One
- How to Set the Record Fields: Use separate resources, and literal values
- Object: Select your new Platform Event, Invoice Posted Event.
- Set Field Values: Map the fields you created on your event.
- Invoice ID (your event field) Equals $Record > Id (the ID of the invoice that triggered the flow).
- Save and Activate
Save your flow and activate it. That’s it!
You org is now generating a salesforce event whenever an Invoice is posted. All we now need is a middleware to read this message and consume it.
Consume Salesforce Event
import queue
import threading
import time
from salesforce_streaming import SalesforceStreamingClient
# --- Configuration (Same as before) ---
SF_USERNAME = "your.name@example.com"
SF_PASSWORD = "your_password"
SF_SECURITY_TOKEN = "your_security_token"
IS_SANDBOX = False
CONSUMER_KEY = "YOUR_CONNECTED_APP_CONSUMER_KEY"
CONSUMER_SECRET = "YOUR_CONNECTED_APP_CONSUMER_SECRET"
EVENT_API_NAME = "My_Custom_Event__e"
# 1. Create a thread-safe queue to hold incoming event payloads
event_queue = queue.Queue()
# 2. Modify the callback to put messages into the queue
def queueing_callback(message):
"""Callback to add the event payload to a processing queue."""
print("🚀 New event received by listener...")
event_payload = message.get('payload', {})
event_queue.put(event_payload) # <-- Put the payload into the queue
def process_events_from_queue():
"""
This is the 'worker' function.
It runs in a separate thread and processes payloads from the queue.
"""
print("✅ Worker thread started. Waiting for events...")
while True:
try:
# The .get() method will wait here until an item is in the queue
payload = event_queue.get(block=True)
print("\n--- Worker picked up an event for processing ---")
print(f" Payload: {payload}")
# --- THIS IS WHERE YOU ADD YOUR PROCESSING LOGIC ---
# For example, get a specific field and call another system.
invoice_id = payload.get('Invoice_ID__c')
if invoice_id:
print(f" Action: Calling another API for Invoice ID: {invoice_id}")
# e.g., result = composite_api_call(invoice_id)
# Simulate work being done
time.sleep(2)
print("--- Finished processing event ---\n")
except Exception as e:
print(f"Error in worker thread: {e}")
# --- Main script logic ---
if __name__ == "__main__":
# 3. Start the worker thread
# The 'daemon=True' means the thread will exit when the main script exits.
worker_thread = threading.Thread(target=process_events_from_queue, daemon=True)
worker_thread.start()
# Initialize and subscribe the Salesforce client
client = SalesforceStreamingClient(
consumer_key=CONSUMER_KEY,
consumer_secret=CONSUMER_SECRET,
username=SF_USERNAME,
password=SF_PASSWORD + SF_SECURITY_TOKEN,
sandbox=IS_SANDBOX
)
channel = f"/event/{EVENT_API_NAME}"
print(f"Listener subscribing to channel: {channel}")
client.subscribe(channel, queueing_callback)
# Start listening
print("Listener connected. Press Ctrl+C to stop.")
try:
client.connect()
except KeyboardInterrupt:
print("Script terminated gracefully.")
client.disconnect()
Extract Invoice and associated details using the event message
From the event, we can extract the Invoice ID and use it to call a composite API in Salesforce org to receive the details of the invoice. Note: You can modify the request body to add/remove more fields and objects as needed.
Here is an example of Composite API Call Request Body:
POST: /services/data/v64.0/composite
{
"allOrNone": false,
"compositeRequest": [
{
"method": "GET",
"url": "/services/data/v62.0/sobjects/Invoice/3ttSG000000Q0SbYAK",
"referenceId": "Invoice"
},
{
"method": "GET",
"url": "/services/data/v62.0/query?q=SELECT+Id,Name,InvoiceId,ChargeAmount,Quantity,UnitPrice,LegalEntityId+FROM+InvoiceLine+WHERE+InvoiceId='3ttSG000000Q0SbYAK'",
"referenceId": "InvoiceLines"
},
{
"method": "GET",
"url": "/services/data/v62.0/query?q=SELECT+Id,TaxName,InvoiceLineId,TaxAmount,TaxCode+FROM+InvoiceLineTax+WHERE+InvoiceLineId+IN+(SELECT+Id+FROM+InvoiceLine+WHERE+InvoiceId='3ttSG000000Q0SbYAK')",
"referenceId": "InvoiceLineTaxes"
},
{
"method": "GET",
"url": "/services/data/v62.0/query?q=SELECT+Id,Name,BillingStreet,BillingCity,BillingPostalCode,BillingCountry+FROM+Account+WHERE+Id+IN+(SELECT+BillingAccountId+FROM+Invoice+WHERE+Id='3ttSG000000Q0SbYAK')",
"referenceId": "Account"
},
{
"method": "GET",
"url": "/services/data/v62.0/query?q=SELECT+Id,FirstName,LastName,Email,Phone+FROM+Contact+WHERE+Id+IN+(SELECT+BillToContactId+FROM+Invoice+WHERE+Id='3ttSG000000Q0SbYAK')",
"referenceId": "Contact"
},
{
"method": "GET",
"url": "/services/data/v62.0/query?q=SELECT+Id,Name,CompanyName,LegalEntityCity,LegalEntityAddress,LegalEntityCountry,Status+FROM+LegalEntity+WHERE+Id+IN+(SELECT+LegalEntityId+FROM+InvoiceLine+WHERE+InvoiceId='3ttSG000000Q0SbYAK')",
"referenceId": "LegalEntity"
}
]
}
Note: Please add and remove more fields/objects in the request as necessary.
Transform the request for Vertex Netherlands
Here is an example of Netherlands request for Vertex:
Here is an example request for Netherlands Invoice Request. https://developer.vertexinc.com/einvoicing/docs/netherlands-examples
Here is an explanation of the fields needed in the Netherlands Invoice Request
| XML Path (Key) | Description / Purpose | Salesforce Availability |
| Routing & Header | ||
| RoutingDetails > Sender | The sender’s VAT registration number. | Yes |
| RoutingDetails > Receiver | A static code telling Vertex the target format (e.g., Netherlands PEPPOL). | In Middleware |
| CustomizationID | A Vertex-specific ID for the billing specification. Static Value: urn:vertexinc:vrbl:billing:1 | Static Value: urn:vertexinc:vrbl:billing:1 |
| ProfileID | A Vertex-specific ID for the business process profile. Static Value: urn:vertexinc:vrbl:billing:1.0 | Static Value: urn:vertexinc:vrbl:billing:1.0 |
| ID | Your unique invoice number. | Yes |
| IssueDate | The date the invoice was created. | Yes |
| IssueTime | The time the invoice was created. | Yes |
| DueDate | The date payment is due. | Yes |
| InvoiceTypeCode | The standard code for the document type (e.g., 380 for an invoice). | In Middleware: 380-Invoice , 381-Credit Note |
| Note | A general, free-text note for the entire invoice. | Yes |
| DocumentCurrencyCode | The three-letter ISO currency code (e.g., EUR). | Yes |
| BuyerReference | A reference number from the buyer (e.g., a contact person or department code). | Yes |
| InvoicePeriod > StartDate | The start date of the service or billing period. | Yes |
| InvoicePeriod > EndDate | The end date of the service or billing period. | Yes |
| InvoicePeriod > Description | A text description of the billing period (e.g., “Monthly”). | Yes |
| OrderReference > ID | The buyer’s Purchase Order (PO) Number. | Yes |
| OrderReference > SalesOrderID | The seller’s internal sales order number. | Yes |
| DespatchDocumentReference > ID | The reference number for the despatch or delivery note. | Yes |
| Supplier (Your Company) Details | ||
| AccountingSupplierParty > EndpointID | Your company’s PEPPOL ID for electronic routing. | Company Specific, in Middleware |
| AccountingSupplierParty > PartyIdentification > ID | Your company’s identifier (often the VAT number). | Yes |
| AccountingSupplierParty > PartyName > Name | Your company’s common trading name. | Yes |
| AccountingSupplierParty > PostalAddress > StreetName | Your company’s street address. | Yes |
| AccountingSupplierParty > PostalAddress > CityName | Your company’s city. | Yes |
| AccountingSupplierParty > PostalAddress > PostalZone | Your company’s postal code. | Yes |
| AccountingSupplierParty > PostalAddress > Country > IdentificationCode | Your company’s two-letter country code (e.g., NL). | Yes |
| AccountingSupplierParty > PartyTaxScheme > CompanyID | Your company’s VAT registration number. | Yes |
| AccountingSupplierParty > PartyLegalEntity > RegistrationName | Your company’s full, official registered name. | Yes |
| AccountingSupplierParty > PartyLegalEntity > CompanyID | Your company’s official registration number (e.g., Chamber of Commerce). | Yes |
| AccountingSupplierParty > Contact > Name | The name of a contact person or department at your company. | Yes |
| Customer (Buyer) Details | ||
| AccountingCustomerParty > EndpointID | The customer’s PEPPOL ID for electronic routing. | Custom Field on Account Object |
| AccountingCustomerParty > PartyName > Name | The customer’s company name. | Yes |
| AccountingCustomerParty > PostalAddress > StreetName | The customer’s street address. | Yes |
| AccountingCustomerParty > PostalAddress > CityName | The customer’s city. | Yes |
| AccountingCustomerParty > PostalAddress > PostalZone | The customer’s postal code. | Yes |
| AccountingCustomerParty > PostalAddress > Country > IdentificationCode | The customer’s two-letter country code. | Yes |
| AccountingCustomerParty > PartyTaxScheme > CompanyID | The customer’s VAT registration number. | Yes |
| AccountingCustomerParty > PartyLegalEntity > RegistrationName | The customer’s full, official registered name. | Yes |
| AccountingCustomerParty > PartyLegalEntity > CompanyID | The customer’s official registration number. | Yes |
| AccountingCustomerParty > Contact > Telephone | The customer’s telephone number. | Yes |
| AccountingCustomerParty > Contact > ElectronicMail | The customer’s email address. | Yes |
| Delivery & Payment | ||
| Delivery > ActualDeliveryDate | The actual date goods were delivered or services were completed. | Order Start Date |
| Delivery > DeliveryLocation > Address > StreetName | The street address of the delivery location. | Yes |
| Delivery > DeliveryLocation > Address > CityName | The city of the delivery location. | Yes |
| Delivery > DeliveryParty > PartyName > Name | The name of the party receiving the delivery. | Yes |
| PaymentMeans > PaymentMeansCode | A standard code for the payment method. | https://developer.vertexinc.com/einvoicing/docs/netherlands-payment-means |
| PaymentMeans > PayeeFinancialAccount > ID | Your bank account number (IBAN) for payment. | Middleware or Custom Object in Salesforce |
| PaymentMeans > PayeeFinancialAccount > Name | The name of the bank account owner. | Middleware or Custom Object in Salesforce |
| PaymentMeans > PayeeFinancialAccount > FinancialInstitutionBranch > ID | Your bank’s identifier code (BIC/SWIFT). | Middleware or Custom Object in Salesforce |
| PaymentTerms > Note | A text note describing the payment terms. | Middleware or Custom Object in Salesforce |
| Invoice Line Items | ||
| InvoiceLine > ID | The unique identifier for the invoice line (line number). | Yes |
| InvoiceLine > Note | A free-text description for the line item. | Yes |
| InvoiceLine > InvoicedQuantity | The quantity of the item sold. | Yes |
| InvoiceLine > LineExtensionAmount | The total amount for the line (Quantity x Price), excluding tax. | Yes |
| InvoiceLine > OrderLineReference > LineID | The corresponding line number from the original purchase order. | Yes |
| InvoiceLine > Item > Name | The name of the product or service. | Yes |
| InvoiceLine > Item > SellersItemIdentification > ID | Your internal product code or SKU. | Yes |
| InvoiceLine > Item > StandardItemIdentification > ID | A standard product identifier like a GTIN or EAN code. | Custom Field on Object |
| InvoiceLine > Item > ClassifiedTaxCategory > ID | A standard code for the tax category (e.g., ‘S’ for standard rate). | https://developer.vertexinc.com/einvoicing/docs/classifiedtaxcategory |
| InvoiceLine > Item > ClassifiedTaxCategory > Percent | The tax rate percentage applicable to this item. | Calculated in Middleware |
| InvoiceLine > Item > AdditionalItemProperty > Name | The name of an additional property for the item. | Yes |
| InvoiceLine > Item > AdditionalItemProperty > Value | The value of the additional property (e.g., the PO number again). | Yes |
| InvoiceLine > Price > PriceAmount | The net unit price of the item (price for one piece). | Yes |
| InvoiceLine > Price > BaseQuantity | The quantity the unit price applies to (usually 1). | Middleware |
| Invoice Totals | ||
| TaxTotal > TaxAmount | The total VAT/tax amount for the entire invoice. | Yes |
| TaxSubtotal > TaxableAmount | The total base amount subject to a specific tax rate. | Yes |
| TaxSubtotal > TaxAmount | The total tax amount for that specific tax rate. | – |
| LegalMonetaryTotal > LineExtensionAmount | The subtotal of all line amounts, before tax and document-level charges. | Yes |
| LegalMonetaryTotal > TaxExclusiveAmount | The total amount of the invoice, excluding tax. | Yes |
| LegalMonetaryTotal > TaxInclusiveAmount | The total amount of the invoice, including tax. | Yes |
| LegalMonetaryTotal > AllowanceTotalAmount | The total of all document-level discounts/allowances. | – |
| LegalMonetaryTotal > ChargeTotalAmount | The total of all document-level extra charges/fees. | – |
| LegalMonetaryTotal > PayableAmount | The final, total amount due for the invoice. | Yes |
Post the request for Vertex Netherlands
This is code in python
"""
Simple script to:
1) get an access token from Vertex
2) POST a VRBL/UBL XML invoice to Vertex Send Document endpoint
"""
import argparse
import json
import os
import sys
import uuid
import time
import requests
DEFAULT_TOKEN_URL = "https://auth.vertexcloud.com/oauth/token"
DEFAULT_BASE_URL = "https://e-invoicing-service.vertexcloud.com"
def get_access_token(client_id: str, client_secret: str, token_url: str = DEFAULT_TOKEN_URL, audience: str = "verx://migration-api", timeout: int = 10):
"""
Request an access token using client_credentials.
Returns (access_token, expires_in, retrieved_at_timestamp)
See Vertex docs for required params. :contentReference[oaicite:4]{index=4}
"""
payload = {
"client_id": client_id,
"client_secret": client_secret,
"audience": audience,
"grant_type": "client_credentials"
}
headers = {"Content-Type": "application/json"}
resp = requests.post(token_url, json=payload, headers=headers, timeout=timeout)
resp.raise_for_status()
j = resp.json()
token = j.get("access_token")
expires_in = j.get("expires_in", 0)
if not token:
raise RuntimeError(f"Failed to obtain access_token. Response: {j}")
return token, int(expires_in), int(time.time())
def send_document(xml_path: str, access_token: str, base_url: str = DEFAULT_BASE_URL, idempotency_key: str = None, timeout: int = 30):
"""
Send a single VRBL XML file to Vertex Send Document endpoint.
The VRBL XML must be posted as raw XML with Content-Type=application/xml. :contentReference[oaicite:5]{index=5}
Returns requests.Response
"""
if idempotency_key is None:
idempotency_key = str(uuid.uuid4())
url = base_url.rstrip("/") + "/customers/v2/documents"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/xml",
"Accept": "application/json",
"Idempotency-Key": idempotency_key
}
with open(xml_path, "rb") as fh:
xml_bytes = fh.read()
resp = requests.post(url, data=xml_bytes, headers=headers, timeout=timeout)
return resp, idempotency_key
def main():
parser = argparse.ArgumentParser(description="Get Vertex access token and send VRBL XML invoice.")
parser.add_argument("--xml-file", required=True, help="Path to VRBL XML invoice file (UBL XML).")
parser.add_argument("--client-id", default=os.getenv("VERTEX_CLIENT_ID"), help="OAuth client_id (or set VERTEX_CLIENT_ID).")
parser.add_argument("--client-secret", default=os.getenv("VERTEX_CLIENT_SECRET"), help="OAuth client_secret (or set VERTEX_CLIENT_SECRET).")
parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Base URL for send document endpoint.")
parser.add_argument("--token-url", default=DEFAULT_TOKEN_URL, help="OAuth token URL.")
args = parser.parse_args()
if not args.client_id or not args.client_secret:
print("Error: client_id and client_secret must be provided either via args or env vars VERTEX_CLIENT_ID and VERTEX_CLIENT_SECRET.", file=sys.stderr)
sys.exit(2)
# 1) Get token
try:
token, expires_in, retrieved_at = get_access_token(args.client_id, args.client_secret, token_url=args.token_url)
expiry_time = retrieved_at + expires_in
print(f"Obtained access token (expires in {expires_in}s at epoch {expiry_time})")
except Exception as e:
print(f"Failed to obtain access token: {e}", file=sys.stderr)
sys.exit(1)
# 2) Send document
try:
resp, idem_key = send_document(args.xml_file, token, base_url=args.base_url)
except Exception as e:
print(f"Error sending document: {e}", file=sys.stderr)
sys.exit(1)
# 3) Process response
print(f"Idempotency-Key used: {idem_key}")
print(f"HTTP {resp.status_code}")
# Vertex typically returns JSON for the response; print it if possible
content_type = resp.headers.get("Content-Type", "")
if "application/json" in content_type:
try:
print(json.dumps(resp.json(), indent=2))
except Exception:
print(resp.text)
else:
print(resp.text)
if not resp.ok:
sys.exit(3)
if __name__ == "__main__":
main()
Update the Org with the Response
Here is an example of how you can update back your org:
PATCH: {instance_url}/services/data/{version}/sobjects/Invoice/{INVOICE_ID}
{
"Document_ID__c": "123e4567-e89b-12d3-a456-426614174000",
"E-invoicing_Status__c": "Sent to Network",
"Idempotency_Key__c": "2a8f6d0c-1f5a-4b6b-b9a0-3f4e3c9d7f12"
}
Poll Vertex for Approval
Vertex Document: https://developer.vertexinc.com/einvoicing/docs/get-document-status
GET: https://e-invoicing-service.vertexcloud.com/customers/v2/documents/{documentId}
Capture fields to update the salesforce org on Approval:
| Output | Type | Description |
| documentId | GUID | The GUID of the document you requested to view the status of. |
| Document Number | String | The logical invoice number from within the invoice body. |
| Issue Date | DateTime | The issue date time from the invoice body. |
| Status | StatusCode | The document workflow status as listed in Document Workflow Status. |
| Transmission DateTime | DateTime | The timestamp at which the invoice was transmitted to Vertex e-Invoicing. |
| ErrorDetails | Object | An object providing structured details about any errors encountered in document processing. |
| Endpoints | Object | An object providing structured information about the routing information for the document. |
Follow the same API call made above to update the Salesforce Org with the exact status and other relevant information.
Transaction Journals
Considering you implement the accounting features as outlined here https://help.salesforce.com/s/articleView?id=ind.billing_financial_accounting.htm&type=5 ; the system will automatically generate the transaction journals for your transactions. Key things to configure
- Enable accounting under Billing Settings
- Setup you Legal Entity
- Setup your Accounting periods and assign to Legal Entity
- Setup your Chart of Accounts (General Ledger Accounts)
- Define the automations to generate journal entries (Setup General Ledger Accounting Assignment Rules)


