Invoice Scripts
Table of Contents
- Overview
- When Invoice Scripts Run
- Actual Usage Opt-In
- Context Variables
- invoice_item_source
- actualusage
- reservation
- Grouping Behaviour for Actual Usage Invoice Items
- Output Format
- invoice Optional
- invoice_items Optional
- Return Behaviour
- Examples
- Example 1: Modifying the Default Reservation Invoice Item
- Example 2: Adding Multiple Reservation Invoice Items
- Example 3: Conditional Invoicing Based on User and Multi-Resource Reservations
- Example 4: Modifying Invoice Properties
- Example 5: Dynamic Pricing Based on Reservation Quantity
- Example 6: Supporting Both Reservations and Actual Usage
- Example 7: Actual Usage Duration-Based Billing
- Example 8: Actual Usage Primary Item Plus Extra Fee
- Best Practices
Overview
Invoice Scripts are powerful tools that allow you to customize how invoice items are generated when a reservation is added to an invoice. By attaching an Invoice Script to a resource, you can dynamically modify invoice items, add additional items, or remove items entirely based on custom business logic.
Invoice Scripts can also optionally run when invoice items are generated from Actual Usage records. This is opt-in because existing invoice scripts often assume that a reservation context variable is present. When a script is enabled for Actual Usage invoice items, the script receives an actualusage context variable instead of reservation.
When Invoice Scripts Run
Invoice Scripts are triggered when:
- A reservation for a resource with an attached Invoice Script is added to an invoice
- The system automatically processes reservations for invoicing
- Actual Usage records are added to an invoice, but only when the script has explicitly enabled Run this script for actual usage invoice items
By default, existing Invoice Scripts only run for reservation-based invoice items.
Actual Usage Opt-In
Invoice Scripts do not automatically run for Actual Usage invoice items. To support Actual Usage, enable the script setting:
Run this script for actual usage invoice items
When this setting is enabled:
- The script can run for reservation-created invoice items and Actual Usage-created invoice items
invoice_item_sourceis always available and identifies the source- For reservation invoice items,
invoice_item_sourceis'reservation' - For Actual Usage invoice items,
invoice_item_sourceis'actualusage' reservationis only available wheninvoice_item_source == 'reservation'actualusageis only available wheninvoice_item_source == 'actualusage'
Scripts that support both sources should check invoice_item_source before reading reservation or actualusage.
Context Variables
Invoice Scripts have access to the following global variables:
| Variable | Availability | Description |
|---|---|---|
invoice |
Always | Information about the invoice being generated |
invoice_items |
Always | The default invoice items that would be created without scripting |
invoice_item_source |
Always | Either 'reservation' or 'actualusage' |
reservation |
Reservation invoice items only | Details about the reservation being invoiced |
parent_reservation |
Reservation invoice items only | Parent reservation details when the current reservation is part of a multi-resource reservation |
actualusage |
Actual Usage invoice items only | Details about the Actual Usage record being invoiced |
invoice_item_source
invoice_item_source is always present. It tells your script what type of source object is being invoiced.
Possible values:
| Value | Meaning |
|---|---|
'reservation' |
The invoice item is being created from a reservation |
'actualusage' |
The invoice item is being created from an Actual Usage record |
Example:
if invoice_item_source == 'reservation':
resource_name = util.dicts.get(d=reservation, path='reservable.name')
if invoice_item_source == 'actualusage':
resource_name = util.dicts.get(d=actualusage, path='reservable.name')
actualusage
When invoice_item_source == 'actualusage', the actualusage variable contains information about the Actual Usage record being invoiced.
It is a single object for the current script invocation, not a list.
Common fields include:
| Field | Type | Description |
|---|---|---|
actual_usage_id |
string/null | ID of the Actual Usage record. This may be null for merged Actual Usage objects |
duration |
number | Duration in seconds |
start |
datetime | Start time |
end |
datetime/null | End time |
in_use |
boolean | Whether the usage is currently active |
reservable |
dictionary | Resource details |
reservable_id |
string | ID of the resource |
source |
string | Source of the Actual Usage record |
user_id |
string | ID of the user, when available |
user_identifier_string |
string | External or unmatched user identifier, when available |
reservation_id |
string | Linked reservation ID, when available |
metrics |
list | Associated Actual Usage metrics |
Example:
if invoice_item_source == 'actualusage':
duration_hours = actualusage['duration'] / 3600
resource_name = util.dicts.get(d=actualusage, path='reservable.name')
Merged Actual Usage Records
In some Actual Usage workflows, multiple Actual Usage records may be merged before invoicing. In that case, actualusage is still a single object, but it may contain:
| Field | Type | Description |
|---|---|---|
actual_usage_id |
null | Merged Actual Usage objects do not have a single record ID |
num_merged |
integer | Number of underlying records |
records |
list | Individual Actual Usage records included in the merged object |
Example:
if invoice_item_source == 'actualusage':
if util.dicts.get(d=actualusage, path='records') != None:
records = actualusage['records']
# Handle merged records
else:
# Handle a single Actual Usage record
duration = actualusage['duration']
reservation
When invoice_item_source == 'reservation', the reservation variable contains information about the Actual Usage record being invoiced.
Refer to the Reservation data dictionary for details on the reservation context variable shape..
Grouping Behaviour for Actual Usage Invoice Items
When invoice items are generated from Actual Usage records, the script runs once per Actual Usage source object.
For example, if 3 Actual Usage records are being invoiced, the script runs 3 times:
Actual Usage 1 -> script runs with actualusage = Actual Usage 1
Actual Usage 2 -> script runs with actualusage = Actual Usage 2
Actual Usage 3 -> script runs with actualusage = Actual Usage 3
The first returned item from each script run can still be grouped into a single invoice item when the system is grouping Actual Usage invoice items by resource.
If the primary returned items have the same grouping fields, they will be combined into one invoice item with accumulated units. That invoice item will reference all associated Actual Usage records.
When scripts run for Actual Usage, grouping considers fields such as:
- Resource
- User
- Item name
- SKU
- Unit rate
- Unit name
- Tax code
- Tax rate
- Tax note
Additional returned invoice items are not grouped. If a script returns multiple invoice items, only the first item is eligible for grouping. Items after the first are created separately for each script invocation.
For example, if 3 Actual Usage records each return:
return {
'invoice_items': [
primary_item,
extra_item
]
}
The result may be:
1 grouped primary invoice item
3 separate extra invoice items
Each invoice item will reference the Actual Usage record or records that created it.
Output Format
An Invoice Script must return a dictionary with the following structure:
return {
'invoice': {
# Optional invoice modifications
},
'invoice_items': [
# Optional invoice item list
]
}
invoice Optional
A dictionary containing modifications to make to the invoice. This can include:
| Field | Type | Description |
|---|---|---|
accepted_notification_emails |
string | Emails to send notifications of acceptance to |
bill_to_account_number |
string | Account number for the billed party |
bill_to_address |
string | Billing address |
bill_to_email |
string | Email address for billing |
bill_to_name |
string | Name to display on the invoice |
bill_to_user_id |
string | 32-character alphanumeric ID of the user to bill |
custom_fields |
dictionary | Custom invoice field values |
deposit_amount |
integer | Requested deposit amount in cents |
discount_rate |
integer | Discount to apply to invoice in hundredths of a percent. 12.55% is represented as 1255 |
due_date |
datetime/string | Due date |
enable_bambora |
boolean | Enable Worldline/Bambora payments. Requires existing integration |
enable_portico |
boolean | Enable Heartland/Portico payments. Requires existing integration |
enable_stripe |
boolean | Enable Stripe payments. Requires existing integration |
hidden_note |
string | Internal note, not visible to customer |
invoice_address |
string | Address of the invoicing entity |
invoice_date |
datetime/string | Invoice date |
invoice_number_prefix |
string | Invoice number prefix |
memo |
string | Memo to display on the invoice |
net_days |
integer | Number of days until payment is due |
payment_instructions |
string | Instructions for payment |
purchase_order_number |
string | PO number reference |
request_acceptance |
boolean | Request the invoice or quote to be accepted |
status |
string | Status of the invoice |
status_detail |
string | Additional status details |
invoice_items Optional
A list of invoice items to include. Each item is a dictionary with:
| Field | Type | Description |
|---|---|---|
date |
string/datetime | Date for the item |
item |
string | Short name of the item |
sku |
string | Stock Keeping Unit identifier |
description |
string | Detailed description of the item |
units |
float/integer | Quantity of units |
unit_rate |
float/integer | Price per unit |
units_name |
string | Name of the unit, such as "hours" or "sessions" |
tax_rate |
integer | Tax rate to apply, stored as 10000 * rate |
tax_code |
string | Tax code for the item |
tax_note |
string | Additional tax information |
Note
Tax rates are stored as 10000 * rate, where rate is the percentage rate of a tax rate. For example, a tax rate of 13.4% is stored as 10000 * 13.4 = 134000.
Return Behaviour
- If you return
{}: The default invoice item will be used - If you return
{'invoice_items': []}: No invoice items will be created - If you do not include
invoice_itemsin the return: The default invoice item will be used - If you include
invoice_itemswith items: Only the items you specify will be used, replacing the default items for that script invocation
Examples
Example 1: Modifying the Default Reservation Invoice Item
# This script modifies the description and adds a tax rate to the default invoice item.
# Get information from the reservation
resource_name = util.dicts.get(d=reservation, path='reservable.name')
hours_reserved = util.dicts.get(d=reservation, path='duration') / 3600
# Create a more detailed description
detailed_description = "Reservation of " + str(resource_name) + " for " + str(hours_reserved) + " hours"
# Get the default invoice item
modified_item = invoice_items[0]
# Update fields in the default item
modified_item = modified_item + {'description': detailed_description}
modified_item = modified_item + {'tax_rate': 70000} # 7% tax rate
modified_item = modified_item + {'tax_code': 'EQUIPMENT-7'}
# Return the modified item
return {
'invoice_items': [modified_item]
}
Example 2: Adding Multiple Reservation Invoice Items
# This script splits a reservation into multiple invoice items,
# for example, base fee plus hourly charges.
# Extract required information
duration_hours = util.dicts.get(d=reservation, path='duration') / 3600
resource_name = util.dicts.get(d=reservation, path='reservable.name')
base_rate = 50.00
hourly_rate = 25.00
# Create list for invoice items
items = []
# Add base fee item
base_item = {
'item': 'Base Fee',
'description': "Setup fee for " + str(resource_name),
'units': 1,
'unit_rate': base_rate,
'units_name': 'fee',
'tax_rate': 70000
}
items = items + [base_item]
# Add hourly charges item
hourly_item = {
'item': 'Usage Fee',
'description': "Usage of " + str(resource_name) + " (" + str(duration_hours) + " hours)",
'units': duration_hours,
'unit_rate': hourly_rate,
'units_name': 'hours',
'tax_rate': 70000
}
items = items + [hourly_item]
# Return both items
return {
'invoice_items': items
}
Example 3: Conditional Invoicing Based on User and Multi-Resource Reservations
# This script applies different billing rules based on the user type.
# Get user information
user_id = util.dicts.get(d=reservation, path='reserved_for.user_id')
# Define internal user IDs
internal_user_ids = [
'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
'q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2'
]
# Check if user is internal
if user_id in internal_user_ids:
# For internal users, do not create any invoice items
return {
'invoice_items': []
}
else:
# For external users, apply a special rate
modified_items = []
for invoice_item in invoice_items:
modified_item = invoice_item
modified_item = modified_item + {'item': 'External Usage'}
modified_item = modified_item + {'unit_rate': 75.00}
modified_item = modified_item + {
'description': 'External user rate - ' + modified_item['description']
}
modified_items = modified_items + [modified_item]
return {
'invoice_items': modified_items
}
Example 4: Modifying Invoice Properties
# This script modifies both invoice items and invoice properties.
# Current date plus 90 days for due date
today = datetime.now()
due_date = datetime.add(dt=today, days=90)
# Get default item and modify it
modified_item = invoice_items[0]
modified_item = modified_item + {'description': 'Premium service fee'}
# Create return object with invoice modifications
return {
'invoice': {
'payment_instructions': 'Payment due within 90 days. Please include invoice number.',
'memo': 'Thank you for your business!',
'due_date': due_date,
},
'invoice_items': [modified_item]
}
Example 5: Dynamic Pricing Based on Reservation Quantity
# This script adjusts pricing based on the quantity reserved.
# Get reservation details
quantity = util.dicts.get(d=reservation, path='units')
resource_name = util.dicts.get(d=reservation, path='reservable.name')
# Base pricing
base_rate = 100.00
# Apply volume discounts
if quantity >= 10:
discounted_rate = base_rate * 0.8 # 20% discount
discount_text = '20% volume discount applied'
if quantity >= 5 and quantity < 10:
discounted_rate = base_rate * 0.9 # 10% discount
discount_text = '10% volume discount applied'
if quantity < 5:
discounted_rate = base_rate
discount_text = 'Standard rate'
# Create invoice item
item = {
'item': resource_name,
'description': str(resource_name) + ' x ' + str(quantity) + ' - ' + str(discount_text),
'units': quantity,
'unit_rate': discounted_rate,
'units_name': 'units',
'sku': 'RES-' + str(resource_name)
}
# Return the item
return {
'invoice_items': [item]
}
Example 6: Supporting Both Reservations and Actual Usage
# This script supports both reservation invoice items and Actual Usage invoice items.
# It checks invoice_item_source before reading reservation or actualusage.
modified_items = []
for invoice_item in invoice_items:
modified_item = invoice_item
if invoice_item_source == 'reservation':
resource_name = util.dicts.get(d=reservation, path='reservable.name')
duration_seconds = util.dicts.get(d=reservation, path='duration')
description_prefix = 'Reservation'
if invoice_item_source == 'actualusage':
resource_name = util.dicts.get(d=actualusage, path='reservable.name')
duration_seconds = actualusage['duration']
description_prefix = 'Actual usage'
duration_hours = duration_seconds / 3600
modified_item = modified_item + {
'item': str(resource_name),
'description': str(description_prefix) + ' for ' + str(duration_hours) + ' hours',
'units': duration_hours,
'units_name': 'hours'
}
modified_items = modified_items + [modified_item]
return {
'invoice_items': modified_items
}
Example 7: Actual Usage Duration-Based Billing
# This script runs for Actual Usage invoice items.
# Enable "Run this script for actual usage invoice items" before using it.
if invoice_item_source == 'actualusage':
modified_items = []
for invoice_item in invoice_items:
duration_hours = actualusage['duration'] / 3600
resource_name = util.dicts.get(d=actualusage, path='reservable.name')
modified_item = invoice_item
modified_item = modified_item + {'item': 'Actual Usage - ' + str(resource_name)}
modified_item = modified_item + {'description': 'Billed from Actual Usage duration'}
modified_item = modified_item + {'units': duration_hours}
modified_item = modified_item + {'units_name': 'hours'}
modified_items = modified_items + [modified_item]
return {
'invoice_items': modified_items
}
# If this script is also attached to reservation invoice items,
# leave reservation items unchanged.
return {}
Example 8: Actual Usage Primary Item Plus Extra Fee
# This script returns a primary item and an extra fee for Actual Usage invoice items.
# The first returned item may be grouped with primary items from other Actual Usage records.
# The extra fee item will be created separately for each Actual Usage record.
if invoice_item_source == 'actualusage':
items = []
for invoice_item in invoice_items:
primary_item = invoice_item + {
'item': 'Actual Usage',
'description': 'Usage duration: ' + str(actualusage['duration']) + ' seconds'
}
extra_item = {
'item': 'Processing Fee',
'description': 'Per Actual Usage record processing fee',
'units': 1,
'unit_rate': 5.00,
'units_name': 'fee'
}
items = items + [primary_item]
items = items + [extra_item]
return {
'invoice_items': items
}
return {}
Best Practices
Check
invoice_item_source: If a script may run for both reservations and Actual Usage, always checkinvoice_item_sourcebefore accessingreservationoractualusage.Opt in carefully: Only enable Actual Usage execution for scripts that are written to handle
actualusage. Existing scripts that assumereservationis always present should not be opted in until they are updated.Start from default items: When modifying default invoice items, start with
invoice_items[0]or loop overinvoice_itemsso default fields like date, resource name, taxes, and rates are preserved.Understand Actual Usage grouping: For Actual Usage invoice items, the first returned item can be grouped across multiple Actual Usage records. Additional returned items are created separately per script invocation.
Test with multiple records: Test Actual Usage scripts with one record, multiple records that group together, and records that should not group together.
Use consistent grouping fields: If you want Actual Usage primary items to group, keep fields like
item,sku,unit_rate,units_name,tax_code,tax_rate, andtax_noteconsistent.Use separate items intentionally: If you return extra invoice items after the first item, expect them to be separate invoice items for each Actual Usage record.
Document custom billing rules: Keep documentation of your custom billing rules for reference by your team.