Documentation

Invoice Scripts

Table of Contents

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_source is always available and identifies the source
  • For reservation invoice items, invoice_item_source is 'reservation'
  • For Actual Usage invoice items, invoice_item_source is 'actualusage'
  • reservation is only available when invoice_item_source == 'reservation'
  • actualusage is only available when invoice_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_items in the return: The default invoice item will be used
  • If you include invoice_items with 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

  1. Check invoice_item_source: If a script may run for both reservations and Actual Usage, always check invoice_item_source before accessing reservation or actualusage.

  2. Opt in carefully: Only enable Actual Usage execution for scripts that are written to handle actualusage. Existing scripts that assume reservation is always present should not be opted in until they are updated.

  3. Start from default items: When modifying default invoice items, start with invoice_items[0] or loop over invoice_items so default fields like date, resource name, taxes, and rates are preserved.

  4. 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.

  5. Test with multiple records: Test Actual Usage scripts with one record, multiple records that group together, and records that should not group together.

  6. 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, and tax_note consistent.

  7. 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.

  8. Document custom billing rules: Keep documentation of your custom billing rules for reference by your team.