Welcome to the Invoice Radar Plugin Handbook for developers!
This guide will help you create custom plugins to fetch invoices and receipts from various platforms.
Invoice Radar is a document automation tool that helps you fetch, download, and organize invoices and receipts from various platforms.
Learn more about Invoice Radar
Introduction
Getting Started
Plugin Structure
Writing Your First Plugin
Useful Patterns
Steps Reference
Basic knowledge of JSON, HTML, CSS and JavaScript.
A text editor or IDE (e.g., VSCode, Sublime Text).
Invoice Radar installed on macOS or Windows.
Download and Install Invoice Radar:
Request Access to Invoice Radar
Download the Blank Plugin:
Download the Blank Plugin to your local machine.
Rename the file to your-plugin-name.json
.
Put it into a folder of your choice.
Add the Plugin to Invoice Radar:
Open Invoice Radar.
Navigate to settings and choose the Available Plugins
tab.
Choose Choose Plugin Directory
and select the folder where you saved the plugin.
Your plugin should now appear in the list of available plugins.
Plugins for Invoice Radar are written in JSON and follow a specific structure. Each plugin consists of the following sections:
Plugin Description:
Metadata: Basic information about the plugin, such as name, description, and homepage URL.
configSchema: Configuration properties that the plugin may require.
Scraping Steps:
checkAuth: Steps to verify if the user is already authenticated.
startAuth: Steps to initiate the authentication process.
getDocuments: Steps to fetch and download documents.
{ "$schema": "https://raw.githubusercontent.com/invoiceradar/plugins/main/schema.json", "id": "example", "name": "Example Platform", "description": "Short description of the service.", "homepage": "https://example.com", "checkAuth": [ { "action": "navigate", "url": "https://example.com/dashboard" }, { "action": "checkElementExists", "selector": "#logout-button" } ], "startAuth": [ { "action": "navigate", "url": "https://example.com/login" }, { "action": "waitForElement", "selector": "#account-summary", "timeout": 120000 } ], "getDocuments": [ { "action": "navigate", "url": "https://example.com/billing" }, { "action": "extractAll", "selector": ".invoice-row", "variable": "invoice", "fields": { "id": { "selector": ".invoice-id" }, "date": { "selector": ".invoice-date" }, "total": { "selector": ".invoice-total" }, "url": { "selector": ".invoice-download", "attribute": "href" } }, "forEach": [ { "action": "downloadPdf", "url": "{{invoice.url}}", "document": "{{invoice}}" } ] } ] }
The full schema can be found here.
Let's create a simple plugin to fetch invoices from a hypothetical service.
Define Metadata:
This information is used to identify and display the plugin in Invoice Radar. The homepage URL is used to get the favicon of the service.
Note that the id
should be unique and lowercase.
{ "id": "example-service", "name": "Example Service", "description": "Short description of the service.", "homepage": "https://example.com"}
Learn more about metadata fields.
Define Configuration Schema (Optional):
The configuration schema defines which fields are required for the plugin to function. In this example, we need a teamID
and password
to authenticate.
These fields will be displayed to the user when adding the plugin in Invoice Radar.
"configSchema": { "teamID": { "type": "string", "title": "Team ID", "description": "The ID of your team or account to fetch invoices.", "required": true } }
Learn more about configuration schema fields.
Check Authentication:
checkAuth
contains steps to verify if the user is authenticated. This can be done by checking the URL or element existence. The last step inside checkAuth
needs to be a verification step.
These steps are executed when a run is started. If the user is already authenticated, the plugin will skip the authentication process and go directly to fetching documents.
"checkAuth": [ { "action": "navigate", "url": "https://example.com/dashboard" }, { "action": "checkElementExists", "selector": "#logout-button" } ]
Start Authentication:
startAuth
contains steps to initiate the authentication process. This can involve navigating to the login page and waiting for a successful login indicator.
The browser will be visible during the authentication process, allowing the user to interact with the login form.
"startAuth": [ { "action": "navigate", "url": "https://example.com/login" }, { "action": "waitForElement", "selector": "#account-summary", "timeout": 120000 } ]
Scrape Documents:
getDocuments
contains steps to fetch and download documents. This can involve navigating to the billing page, extracting invoice details, and downloading the PDFs.
"getDocuments": [ { "action": "navigate", "url": "https://example.com/billing" }, { "action": "extractAll", "selector": ".invoice-row", "variable": "invoice", "fields": { "id": { "selector": ".invoice-id" }, "date": { "selector": ".invoice-date" }, "total": { "selector": ".invoice-total" }, "url": { "selector": ".invoice-download", "attribute": "href" } }, "forEach": [ { "action": "downloadPdf", "url": "{{invoice.url}}", "document": { "type": "invoice", "id": "{{invoice.id}}", "date": "{{invoice.date}}", "total": "{{invoice.total}}" } } ] } ]
You are done!:
Save the file and add it to Invoice Radar. You can now run the plugin to fetch invoices from the service.
checkAuth
)Many services automatically redirect to the login page if the user is not authenticated. We can use this behavior to check if the user is authenticated.
{ "action": "navigate", "url": "https://example.com/login"}, { "action": "checkURL", "url": "https://example.com/account", }
Depending on the service, they may redirect you from the dashboard to the login page if you are not authenticated. In this case, you can use the checkURL
step to check if the URL still matches after visiting the dashboard.
{ "action": "navigate", "url": "https://example.com/dashboard"}, { "action": "checkURL", "url": "https://example.com/dashboard", }
Note that you can use glob patterns to match dynamic URLs: https://example.com/dashboard/**
.
You can use a selector that is unique to the authenticated state to check if the user is authenticated, e.g. a logout button or profile link.
{ "action": "navigate", "url": "https://example.com/home"}, { "action": "waitForElement", "selector": "#logout-button"}
In some cases, the website has not fully loaded when the checkElementExists
step is executed. To avoid this, you can use the waitForNetworkIdle
attribute to wait for the page to be fully loaded.
{ "action": "navigate", "url": "https://example.com/home", "waitForNetworkIdle": true}, { "action": "checkElementExists", "selector": "#logout-button"}
startAuth
)Most authentication processes start by navigating to the login page and waiting for a specific element to appear after a successful login.
Remember that the browser will be visible during the authentication process, allowing the user to interact with the login form. The authentication flow itself can be automated, but isn't requried.
{ "action": "navigate", "url": "https://example.com/login"}, { "action": "waitForElement", "selector": "#logout-button", "timeout": 120000}
To give the user enough time to log in, it's recommend to provide a long timeout to the wait step, with a default of 120 seconds.
This section provides an overview of the available steps that can be used to create plugins for Invoice Radar. Each step represents a specific action that can be performed during the automation process.
Navigation Steps
Navigate (navigate
)
Wait for URL (waitForURL
)
Wait for Element (waitForElement
)
Wait for Navigation (waitForNavigation
)
Wait for Network Idle (waitForNetworkIdle
)
Interaction Steps
Click Element (click
)
Type Text (type
)
Select Dropdown (dropdownSelect
)
Run JavaScript (runJs
)
Verification Steps
Check Element Exists (checkElementExists
)
Check URL (checkURL
)
Run JavaScript (runJs
)
Data Extraction Steps
Extract (extract
)
Extract All (extractAll
)
Document Retrieval Steps
Download PDF (downloadPdf
)
Wait for PDF Download (waitForPdfDownload
)
Print Page as PDF (printPdf
)
Download Base64 PDF (downloadBase64Pdf
)
Conditional Logic Steps
If (if
)
Miscellaneous Steps
Sleep (sleep
)
Snippets
Get Invoice from Stripe URL (getInvoiceFromStripeUrl
)
Get Invoices from Stripe Customer Portal (getInvoicesFromStripeBillingPortal
)
navigate
)Navigates to the given URL and waits for the page to load. By default, it only waits for the initial page load, not for any subsequent AJAX requests.
{ "action": "navigate", "url": "https://example.com"}
You can set waitForNetworkIdle
to true
to ensure the page is fully loaded before continuing.
{ "action": "navigate", "url": "https://example.com/dashboard", "waitForNetworkIdle": true}
Good to know:
Relative URLs are supported and will be resolved based on the current page.
The navigate action will only wait for the initial page load, not for any subsequent AJAX requests.
waitForURL
)Waits for the current URL to match the given URL, optionally with a timeout. Supports wildcards.
{ "action": "waitForURL", "url": "https://example.com/profile/**", "timeout": 3000}
waitForElement
)Waits for the given selector to appear on the page, optionally with a timeout.
{ "action": "waitForElement", "selector": "#example", "timeout": 3000}
waitForNavigation
)Waits for the page navigation to happen. This step will not wait for the page to be fully loaded. Use the waitForNetworkIdle step for that purpose. Timeout is optional and defaults to 10 seconds
{ "action": "waitForNavigation", "timeout": 10000}
waitForNetworkIdle
)Waits for the network to be idle. This is useful if you want to ensure the page has finished loading all resources. The steps completes when there are no more network requests for 500ms. Timeout is optional and defaults to 15 seconds.
The navigate
step has a waitForNetworkIdle
option that can be set to true
to get the same behavior.
{ "action": "waitForNetworkIdle", "timeout": 10000}
click
)Clicks the element specified by the given selector on the page.
{ "action": "click", "selector": "#button"}
type
)Types the given text into the element specified by the given selector on the page.
{ "action": "type", "selector": "#input", "value": "Hello World"}
dropdownSelect
)Selects the given value from the dropdown specified by the given selector on the page. The selection happens based on the value
attribute of the option.
{ "action": "dropdownSelect", "selector": "#dropdown", "value": "Option 1"}
runJs
)Runs the given JavaScript in the page context. If a promise is returned, it will be awaited.
If you want to use the result of a script in subsequent steps, use the extract step instead.
{ "action": "runJs", "script": "document.querySelector('#example').click();"}
These steps are used inside checkAuth
to verify if the user is authenticated.
checkElementExists
)Checks if the given selector exists on the page. Typically used for authentication checks.
{ "action": "checkElementExists", "selector": "#example"}
checkURL
)Checks if the current URL matches the given URL. Supports wildcards patterns like https://example.com/dashboard/**
.
{ "action": "checkURL", "url": "https://example.com"}
runJs
)The runJs
step can be used as verification step as well. By running a script that returns a truthy or falsy value, you can verify if the user is authenticated.
{ "action": "runJs", "script": "document.cookie.includes('authToken');"}
These steps are used to load data from the page, like a list of items or a single value, and use it in subsequent steps.
extract
)Extracts a single piece of data from the page and stores it in a variable.
Using a CSS fields:
{ "action": "extract", "variable": "account", "fields": { "id": "#team-id", "name": "#team-name", "url": { "selector": "#team-link", "attribute": "href" } } }
In this example account
is used as variable name, and the fields id
, name
, and url
are extracted using CSS selectors. They can be used in subsequent steps using the {{account.id}}
, {{account.name}}
, and {{account.url}}
placeholders.
Using JavaScript:
{ "action": "extract", "variable": "token", "script": "localStorage.getItem('authToken')"}
This example creates a token
variable that is extracted using JavaScript. The value can be accessed using the {{token}}
placeholder. It's also possible to return an object.
extractAll
)Extracts a list of data from the page, and runs the given steps for each item. This is commonly used to iterate over a list of invoices and download them.
For each element matching the selector
, the fields are extracted and stored in the variable
available in the forEach
steps.
Good to know:
Each selector inside the fields
object is automatically scoped to the matched element.
The variable
field is optional. If not provided, the extracted data will be stored in the default variable item
.
The current index can be accessed using the {{index}}
placeholder. It starts at 0 and increments for each item.
With CSS fields:
{ "action": "extractAll", "selector": ".invoice-list .invoice-item", "variable": "invoice", "fields": { "id": "td.invoice-id", "date": "td.invoice-date", "total": "td.invoice-total", "url": { "selector": "a.invoice-link", "attribute": "href" } }, "forEach": [ { "action": "navigate", "url": "{{invoice.url}}" }, { "action": "downloadPdf", "invoice": "{{invoice}}" } ] }
With JavaScript:
When using JavaScript, the result should be an array of objects or values. If the result is a promise, it will be awaited.
{ "action": "extractAll", "script": "Array.from(document.querySelectorAll('#year-selector option')).map(option => option.value);", "variable": "year", "forEach": [ { "action": "dropdownSelect", "selector": "#year-selector", "value": "{{year}}" } ] }
Pagination
Experimental support, not yet documented.
These steps are used to download documents and process them in Invoice Radar. All steps require the document
object to be passed as an argument, which contains the metadata of the document.
The document
argument has the following fields:
Required
id
: The unique document ID
E.g. INV-123
or 123456
date
: The invoice date as string
E.g. 2022-01-01
or 01/01/2022
or January 1, 2022
Recommended
total
: The invoice total amount including the currency.
E.g. $100.00
or €100.00
or 100 EUR
or 100,00€
The built in parser will try to extract the amount and currency from the string.
Optional
type
: The type of the document (Optional. Defaults to auto
)
Can be set to auto
, invoice
, receipt
, refund
or other
.
metadata
: Additional metadata for the document (Optional)
E.g. { "orderNumber": "12345" }
You can either pass every field separately or the whole object if it contains all required fields.
E.g. using separate fields:
"document": { "id": "{{item.invoiceId}}", "date": "{{item.date}}", "total": "{{item.amount}} {{item.currency}}", "type": "invoice"}
E.g. if the object contains all required fields, you can pass it directly:
"document": "{{item}}"
downloadPdf
)Downloads a PDF from the given URL.
{ "action": "downloadPdf", "url": "https://example.com/invoice.pdf", "document": { "id": "{{item.invoiceId}}", "date": "{{item.date}}", "total": "{{item.total}}" } }
waitForPdfDownload
)Waits for a PDF download. Timeout defaults to 15 seconds.
{ "action": "waitForPdfDownload", "timeout": 10000, "document": { "id": "{{item.invoiceId}}", "date": "{{item.date}}", "total": "{{item.total}}" } }
printPdf
)Prints the current page to a PDF file.
{ "action": "printPdf", "document": { "id": "{{item.invoiceId}}", "date": "{{item.date}}", "total": "{{item.total}}" } }
downloadBase64Pdf
)Downloads a PDF from a base64 encoded string.
{ "action": "downloadBase64Pdf", "base64": "{{item.base64String}}", "document": { "id": "{{item.invoiceId}}", "date": "{{item.date}}", "total": "{{item.total}}" } }
if
)Runs the given steps if the condition is true. If the condition is false, the else
steps are executed.
{ "action": "if", "script": "'{{invoice.url}}'.includes('pdf')", "then": [ { "action": "click", "selector": "#example" } ], "else": [ { "action": "navigate", "url": "https://example.com/fallback" } ] }
sleep
)Waits for the given amount of time in milliseconds. This is generally not recommended. In most cases, it's better to use the waitForElement, waitForURL or waitForNetworkIdle steps.
{ "action": "sleep", "duration": 1000}
Snippets are pre-built sets of steps that simplify common tasks. The steps for a specific snippet are visible inside the developer tools
Currently, it's not possible to create custom snippets. If you have a common task that you think would be useful as a snippet, please create an issue on GitHub.
getInvoiceFromStripeUrl
)Extracts an invoice from a Stripe invoice URL.
{ "action": "runSnippet", "snippet": "getInvoiceFromStripeUrl", "args": { "url": "https://invoice.stripe.com/i/inv_123" } }
getInvoicesFromStripeBillingPortal
)Extracts available invoices from a Stripe billing portal.
{ "action": "runSnippet", "snippet": "getInvoicesFromStripeBillingPortal", "args": { "url": "https://stripe-portal.example.com/billing" } }
Sometimes, you might need to run a fetch request inside a step to fetch data from an API. To do this, you can use the extractAll
action.
{ "action": "extractAll", "variable": "invoice", "script": "fetch('https://example.com/api/invoices').then(res => res.json())" "forEach": [ { "action": "downloadPdf", "url": "{{invoice.url}}", "document": { "id": "{{invoice.id}}", "date": "{{invoice.date}}", "total": "{{invoice.total}}" } } ] }
This will run the fetch request and return the result as a JavaScript object.
In some scenarios, you might need to run a step inside an element. To do this, you can use the
iframe
attribute on the step.
{ "action": "click", "selector": "#button-inside-iframe", "iframe": true},
By setting iframe
to true
, Invoice Radar will find the first element on the page and run the step inside it.
You can also use a string that is contained inside the iframe's src
attribute to target a specific iframe.
{ "action": "click", "selector": "#button-inside-iframe", "iframe": "iframe.example.com"},