# Changelog
Source: https://developers.pleo.io/changelog
## Tag Groups API
Tag, tag group, and tag dimension create/update endpoints now return a clean `400 Bad Request` when a `name`, `code`, or `value` field exceeds 255 characters, instead of an unhandled `500` error
## New Brand, New Look, Same Great Docs
We've rebranded the entire Developer Portal to match Pleo's new brand identity, with new typography, colours, logo, icons, illustrations, and a redesigned homepage, in both light and dark mode
Explore the [new homepage](/) to see it in action
## Pleo MCP
New [How to Install the Pleo MCP in ChatGPT Business](/docs/current/pleo-mcp/how-tos/installation/chatgpt-business) guide for publishing and authorising the Pleo MCP in ChatGPT Business, Enterprise, and Edu workspaces
Added a Troubleshooting section to the [Pleo MCP FAQs](/docs/current/pleo-mcp/get-started/faq#chatgpt-business) covering common ChatGPT Business install and connection issues
## Export API Version 3
Added explicit target system support for export jobs:
[**Create a new export job**](/reference/export-api/export-api-v3/create-a-new-export-job) now accepts an optional `targetSystem` field, validated against the company's active connections; supplying an unrecognised value returns a `422` with an `INVALID_TARGET_SYSTEM` error
[**Get a list of export jobs**](/reference/export-api/export-api-v3/get-a-list-of-export-jobs) now supports an optional `target_systems` filter to narrow results to one or more explicit target systems; supplying an unrecognised value returns a `422` with an `INVALID_TARGET_SYSTEM_FILTER` error
[**Get a list of export jobs**](/reference/export-api/export-api-v3/get-a-list-of-export-jobs) and [**Get an export job**](/reference/export-api/export-api-v3/get-an-export-job) responses now include a `targetSystem` field, showing which system each job resolved to
## Pleo MCP
New [Pleo MCP documentation suite](/docs/current/pleo-mcp/get-started/pleo-mcp-overview). You can now connect Pleo to AI assistants like Claude, ChatGPT, Codex, Gemini Enterprise, and Copilot, so users can ask their assistant to handle Pleo tasks directly.
Enable Pleo MCP for your organisation with one of the supported platforms: [Claude](/docs/current/pleo-mcp/how-tos/enablement/claude), [Codex](/docs/current/pleo-mcp/how-tos/enablement/codex), [ChatGPT](/docs/current/pleo-mcp/how-tos/enablement/chatgpt), [Gemini Enterprise](/docs/current/pleo-mcp/how-tos/enablement/gemini-enterprise), or [Copilot](/docs/current/pleo-mcp/how-tos/enablement/copilot)
Enable Pleo MCP per user by connecting it to [Claude](/docs/current/pleo-mcp/how-tos/installation/claude), [Codex](/docs/current/pleo-mcp/how-tos/installation/codex), [ChatGPT](/docs/current/pleo-mcp/how-tos/installation/chatgpt), [Copilot](/docs/current/pleo-mcp/how-tos/installation/copilot), or any [custom MCP-compatible client](/docs/current/pleo-mcp/how-tos/installation/custom-mcp)
[Revoke access](/docs/current/pleo-mcp/how-tos/uninstallation/revoke-access) to disconnect the Pleo MCP when it's no longer needed
[Example Use Cases](/docs/current/pleo-mcp/get-started/use-cases), [FAQs](/docs/current/pleo-mcp/get-started/faq), and [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions) to help users understand what they can do and who can do it
## Enrichment API Reference
Clarified [Enrichment Workflow Quick Guide](/reference/enrichment-api/enrichment-workflow-basic-guide) prerequisites: your integration must already have the receipt, the card's last four digits, and any other matching information before submitting a request
## Export API Version 3
New field `attendeesData` has been added to the Export API version 3. For more information see [GET Get Export Items](/reference/export-api/export-items-v3/get-export-items).
## Developer Portal Docs
New **Imports -> Tax Sync** documentation suite:
New [Platform Capabilities -> Tax Sync Overview](/docs/current/platform/accounting-integrations/imports/tax-codes/tax-sync-overview)
New [Integration Design -> Tax Sync](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-overview) covering the sync execution model, reconciliation behaviour, type mapping, data mapping, and scheduling
New [Tax Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tax-sync-workflow-guide)
New how-to articles with step-by-step instructions, curl examples, and Pleo Web App screenshots:
[How to Fetch and Match Tax Codes](/docs/current/how-tos/accounting-integrations/imports/tax-codes/how-to-fetch-and-match-tax-codes)
[How to Create, Update, and Archive Tax Codes](/docs/current/how-tos/accounting-integrations/imports/tax-codes/how-to-create-update-archive-tax-codes)
New [Classifying Expenses: Tags, Categories, and Tax Codes](/docs/current/platform/accounting-integrations/expense-classification-overview) overview, explaining how Tags Sync, Chart of Accounts Sync, and Tax Sync fit together to classify an expense, with manual Pleo Web App setup and export options
Archived the old **Configuring Customer Account in Pleo** pages (Tax Codes, Categories, Tags) in favour of the new [Classifying Expenses](/docs/current/platform/accounting-integrations/expense-classification-overview) overview
## Developer Portal Docs
New **Imports -> Chart of Accounts Sync** documentation suite:
New [Platform Capabilities -> Chart of Accounts Sync Overview](/docs/current/platform/accounting-integrations/imports/accounts/accounts-sync-overview)
New [Integration Design -> Chart of Accounts Sync](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-overview) covering the sync execution model, reconciliation behaviour, data mapping, and scheduling
New [Chart of Accounts Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/accounts-sync-workflow-guide)
New how-to articles with step-by-step instructions:
[How to Sync Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts): fetch accounts from the AS and Pleo, then match by `externalId`
[How to Create, Update, and Archive Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-create-update-archive-accounts): apply create, unarchive, update, and archive operations
[How to Create Multiple Accounts in a Single Request](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-batch-create-accounts): batch create up to 1000 accounts in one API call
## Accounting Entries API
Accounting Entries API is now available. See [migration guides](/deprecation/legacy-expense-api/overview).
## Developer Portal Docs
New **Imports -> Chart of Accounts Sync** documentation suite:
New [Platform Capabilities -> Chart of Accounts Sync Overview](/docs/current/platform/accounting-integrations/imports/accounts/accounts-sync-overview)
New [Integration Design -> Chart of Accounts Sync](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-overview) covering the sync execution model, reconciliation behaviour, data mapping, and scheduling
New [Chart of Accounts Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/accounts-sync-workflow-guide)
New how-to articles with step-by-step instructions:
[How to Sync Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts): fetch accounts from the AS and Pleo, then match by `externalId`
[How to Create, Update, and Archive Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-create-update-archive-accounts): apply create, unarchive, update, and archive operations
[How to Create Multiple Accounts in a Single Request](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-batch-create-accounts): batch create up to 1000 accounts in one API call
## Accounting Entries API
Updated [Accounting Entries API](/reference/accounting-entries/accounting-entry-overview) Availability Date
## Accounting Entries API
New [Accounting Entries API Reference](/reference/accounting-entries/accounting-entry-overview)
Migration Paths for Deprecated Expense API now Available:
[Migrate to the Export API](/deprecation/legacy-expense-api/migration-guide-expenses-api-to-export-api)
[Migrate to the Accounting Entries API](/deprecation/legacy-expense-api/migrate-to-accounting-entries-api)
New [How to Connect Pleo to a Custom API Integration](/docs/current/how-tos/accounting-integrations/how-to-connect-pleo-to-a-custom-api-integration)
## Enrichment API Reference
New [Enrichment API Overview](/reference/enrichment-api/enrichment-api-overview): key concepts, authentication, request parameters, receipt statuses, and supported features
New [Enrichment Workflow Quick Guide](/reference/enrichment-api/enrichment-workflow-basic-guide): condensed guide covering the three workflow steps and the optional tags path
New [Enrich Accounting Entry v2](/reference/enrichment-api/enrichment-api-v2/enrich-accounting-entry) endpoint reference
## Developer Portal Docs
**AS/ERP Integration** improvements:
Improved look and feel of the [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide) and [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide) to match the Tags Sync guide pattern, with expanded step descriptions, sub-flow diagrams, and black CTA buttons
Added sub-flow diagrams to all [export how-to articles](/docs/current/how-tos/accounting-integrations/how-to-queue-export-items-in-ui), showing the steps for each how-to inline below the top navigation diagram
Updated [**Detect & Start Export Jobs**](/docs/current/integration-design/exports/integration-design-exports-detect-and-start-export-jobs) terminology across all export docs (previously "Detect & Claim") to avoid misinterpretation in external partner accounting workflows
Clarified [eligible job status](/docs/current/integration-design/exports/integration-design-exports-detect-and-start-export-jobs#eligible-job-states) for normal detection: `pending` only; `in_progress` is only included when recovering from an interrupted job
Corrected batch limit for [Update Export Items](/docs/current/integration-design/exports/integration-design-exports-update-export-items) from 10 to 100 items per API request
## Deprecation and Migration
New [**Deprecation and Migration**](/deprecation/overview) section
Updated availability dates across migration plan tables
## Developer Portal Docs
New **Imports -> Tags Sync Workflow Docs**
New [Platform Capabilities & Concepts -> Tags Sync Overview](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview)
New [Integration Design -> Tags Sync Overview](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-overview)
New [Workflow Guides -> Tags Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tags-sync-workflow-guide)
New [Multiple How-tos'with step-by-step instructions on how to sync Tags](/docs/current/how-tos/accounting-integrations/imports/tags/how-to-select-dimensions-for-tags-sync)
## Export API Version 3
New fields `ingoingTaxAccount` and `outgoingTaxAccount` have been added to the Export API version 3. For more information see [GET Get Export Items](/reference/export-api/export-items-v3/get-export-items).
## Export API Version 3
Added support for Swedish representation tax calculations. No schema changes were made to data.accountingEntryLines.tax; however, the underlying tax application logic reflected in the UI has changed to support Swedish entertainment expense deduction rules. For more information, see [Change VAT & tax deductions for entertainment expenses in Sweden
](https://help.pleo.io/en/support/solutions/articles/103000292778-%E4ndra-moms-och-avdrag-vid-representation-i-sverige).
## Developer Portal Docs
Included [expense subtypes](/docs/expense-types): `interest`, `invoice_payment_sent`, `invoice_payment_returned`, `reimbursement_to_employee`, `reimbursement_to_company`, and `reimbursement_to_employee_refund`
## Developer Portal Docs
New **AS/ERP Integration** section covering the full Export workflow:
[Integration Design](/docs/current/integration-design/exports/integration-design-exports-overview) sections for all Export steps including bookkeeping methods, accounts mapping, posting behaviour, attachment handling, and accounting periods
[Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide) and [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
Full suite of [How-to articles](/docs/current/how-tos/accounting-integrations/how-to-queue-export-items-in-ui) covering each step end-to-end
Troubleshooting articles:
[How to Resolve MISSING\_CONTRA\_ACCOUNTS](/docs/current/how-tos/accounting-integrations/how-to-resolve-missing-contra-accounts)
[How to Enable Vendor-Based Bookkeeping](/docs/current/how-tos/accounting-integrations/how-to-enable-vendor-based-bookkeeping)
More improvements coming soon!
## Developer Portal Docs
New [**API Base URLs**](/docs/current/authentication/api-base-urls) article
[Standalone API Key Workflow Guide](/docs/current/guides/standalone-api-keys-workflow-guide) with linked [**How-to**](/docs/current/how-tos/environment-access/how-to-get-access-to-all-env-standalone-api-keys) articles
[How to Make an API Call Using a Standalone API Key (Postman)](/docs/current/how-tos/api-keys/how-to-make-an-api-call-using-standalone-api-keys-postman)
## Export API Version 3
Multi-currency support has been extended to Bill Invoice Payments in the Export API version 3. For more information see [GET Get Export Job Items](/reference/export-api/export-items-v3/get-export-items).
## Chart of Accounts API
`POST Fetch a list of accounts` endpoint has been updated:
`code` is marked as deprecated.
`codes` is a new optional preferred search field.
For more information see [POST Fetch a list of accounts](/reference/accounts/fetch-a-list-of-accounts).
## Chart of Accounts API
New endpoint to create multiple bookkeeping accounts in a single batch operation has been added to the Chart of Accounts API. For more information see [POST Create multiple accounts in a single request](/reference/accounts/create-multiple-accounts-in-a-single-request).
## Export API Version 3
New field subType has been added to the Export API version 3. For more information see [GET Get Export Job Items](/reference/export-api/export-items-v3/get-export-items).
## Export API Version 2
New field subType has been added to the Export API version 2. For more information see [GET Get Export Job Items](/reference/export-api/export-items-v2/get-export-items).
## Chart of Accounts API
New search fields `excludeIfAssignedToContraAccount`, `excludeIfAssignedToCategory` have been added to the Chart of Accounts API. For more information see [POST Fetch a list of accounts](/reference/accounts/fetch-a-list-of-accounts).
## Export API Version 3
New field `servicePeriod` has been added to the Export API version 3. For more information see [GET Get Export Job Items](/reference/export-api/export-items-v3/get-export-items).
## Export API Version 2
New field `servicePeriod` has been added to the Export API version 2. For more information see [GET Get Export Job Items](/reference/export-api/export-items-v2/get-export-items).
## Export API Version 3
New failure reason type `tag_unknown` has been added to the Export API Version 3. For more information see [GET Get Export Job Items](/reference/export-api/export-api-v3/get-export-job-items) and [PUT Update Export Job Items](/reference/export-api/export-api-v3/update-export-job-items).
## Export API Version 3
New failure reason type `tax_code_unknown` has been added to the Export API Version 3. For more information see [GET Get Export Job Items](/reference/export-api/export-api-v3/get-export-job-items) and [PUT Update Export Job Items](/reference/export-api/export-api-v3/update-export-job-items).
## Export API Version 3
New failure reason type `account_unknown` has been added to the Export API Version 3. For more information see [GET Get Export Job Items](/reference/export-api/export-api-v3/get-export-job-items) and [PUT Update Export Job Items](/reference/export-api/export-api-v3/update-export-job-items).
## Export API Version 3
New failure reason type `vendor_unknown` has been added to the Export API Version 3. For more information see [GET Get Export Job Items](/reference/export-api/export-api-v3/get-export-job-items) and [PUT Update Export Job Items](/reference/export-api/export-api-v3/update-export-job-items).
## Companies API
The Companies API is available to search for companies associated with an organisation. You can also retrieve details of a specific company. For more information, see [Companies API Overview](/reference/companies/).
## Chart of Accounts API
The Accounts API is renamed to the **Chart of Accounts** API. For more information, see [Chart of Accounts API Overview](/reference/chart-of-accounts-api-overview).
## Export API Version 3
A new version is released to support multi-currency configuration. For more information, see [Multi-Currency Configuration](/reference/multi-currency-configuration). The significant changes introduced in this version are:
In the endpoints' response, values of enums are now returned as strings.
`GET Get Export Items` endpoint:
`controlAccount` object is returned in the endpoint response.
`teamCode` is removed from the endpoint response.
`GET Get a list of Export Jobs` endpoint:
`status` is removed from the query parameters of the endpoint request.
`statuses` is the query parameter to request jobs in specific states.
## Webhook Subscriptions | Basic and Header-Based Authentication
We now support **Basic** and **Header-Based** authentication methods, in addition to **HMAC signature-based** authentication. For more detailed information, see [Authentication in Webhooks](/reference/overview-webhooks#authentication-in-webhooks). Also, now an integration can send custom headers in a new subscription request — see [Configuring Custom Headers in New Subscription Request](/reference/overview-webhooks#configuring-custom-headers-in-new-subscription-request).
## Export API Version 2
You can now tag vendors to an expense, if the specific company has synchronised vendors between their ERP/accounting system and Pleo. Also, the integration must support vendor tagging. For more information on vendor specific data points that you might receive in an API response, see [POST Get Export Items](/reference/export-api/export-items-v2/get-export-items) \`.
## Accounts API Version 1
The Accounts API is now available for synchronisation of accounts between the ERP/accounting system and Pleo. For more information, see [Accounts API](/reference/chart-of-accounts-api-overview).
## Vendors API Version 1
The Vendors API is now available for synchronisation of vendors records between the ERP/accounting system and Pleo. For more information, see [Vendor API Overview](/reference/vendor/vendors-api-overview).
## Accounting Entries Enrichment API Version 1
The Accounting Entries Enrichment API Version 1 is available for select partners and customers. For more detailed information, see [Accounting Entries Enrichment API Overview](/reference/enrichment-api/enrichment-api-overview).
## Employees API Version 2
The Employees API Version 1 is deprecated and in Version 2, these attributes have been removed: `teamId`, `avatar` and `role`. In addition, you can search for employee details of an organisation or company by `companyId`, `email`, or `code` in Version 2. For more information, see [Employees API Overview](/reference/employees-api-overview).
## Export API Version 2
V1 is operational, but we recommend you to upgrade to V2 for better user experience - the user attribute in `GET Get Export Items` response would be null, if there is no user/employee associated with the export item. For more information, see [Export API](/reference/export-api-overview). Also, V0 would be deprecated in 2025. Closer to the deprecation date, we would inform you.
# Migrate to the Accounting Entries API
Source: https://developers.pleo.io/deprecation/legacy-expense-api/migrate-to-accounting-entries-api
How to migrate from the legacy Expenses API to the Accounting Entries API for custom workflows.
This guide is for partners who used the [Expenses API](/reference/pleo-deprecated/expenses/get-an-expense-for-a-company) to build custom expense-related workflows — for example syncing expenses to project management software, building dashboards, or generating reports. If you were building an accounting export integration, see [Migrate to the Export API](/deprecation/legacy-expense-api/migration-guide-expenses-api-to-export-api) instead.
## New to the Accounting Entries API?
What an Accounting Entry is and how the API is structured.
Required scope: `accounting-entries:read`.
How to generate standalone API keys for authenticating your integration.
End-to-end guide for authenticating with standalone API keys.
How to make API calls using standalone API keys in Postman.
## Getting Started
You'll need API credentials with at least the [`accounting-entries:read`](/reference/accounting-entries/scopes) scope. Once authenticated, the two main endpoints you'll use are:
Search Accounting Entries
Get an Accounting Entry by ID
## Key Differences
* Field names and structure have changed. See [Field Mapping](#field-mapping) below.
* The Accounting Entries API uses cursor-based pagination.
## Field Mapping
The table below maps fields from the `Expense` model to their equivalents in the `AccountingEntry` model.
* `—` : field is not available in the AccountingEntry model
* `*` : data is the same but represented differently. See the [Accounting Entries API reference](/reference/accounting-entries) for details.
| `Expense` field | `AccountingEntry` equivalent |
| :---------------------------- | :---------------------------------- |
| id | id |
| employeeId | employeeId |
| employeeCode | — |
| departmentId | teamId |
| performedAt | performedAt |
| amountOriginal | transactionValue |
| amountSettled | totalBillValue **\*** |
| note | note |
| type | family **\*** |
| accountId | accountId |
| taxCodeId | taxCodeId |
| createdAt | createdAt |
| updatedAt | updatedAt |
| deletedAt | deletedAt |
| tagIds\[].id | tags\[].tagId |
| tagIds\[].tagGroupId | tags\[].tagGroupId |
| receiptIds\[] | receiptIds\[] |
| settledExpenseIds\[] | — |
| status | status **\*** |
| supplier.CIF | supplier.cif |
| supplier.documentNumber | supplier.documentNumber |
| cardTransaction.state | — |
| cardTransaction.authorizedAt | — |
| cardTransaction.settledAt | settledAt |
| cardTransaction.reversedAt | — |
| cardTransaction.merchant.id | — |
| cardTransaction.merchant.name | — |
| lines\[].amountSettled | splitItems\[].totalBillValue **\*** |
| lines\[].accountId | splitItems\[].accountId |
| lines\[].taxCodeId | splitItems\[].taxCodeId |
| lines\[].tagIds\[] | splitItems\[].tags\[].tagId |
| lines\[].accountCode | splitItems\[].accountCode |
Accounting Entries API Overview
## New Fields in AccountingEntryV1
The following fields are available in `AccountingEntryV1` with no equivalent in the Expense API.
| Field | Description |
| :----------------- | :------------------------------------------------------------------- |
| companyId | Company the entry belongs to |
| accountCode | Account code for the entry |
| bookkeepingDate | Date used for accounting records |
| subFamily | Finer-grained classification within `family` |
| exportStatus | Export state of the entry (separate from `status`) |
| reviewStatus | Review state of the entry |
| attendees | People associated with the entry (e.g. attendees of a business meal) |
| foreignExtensionId | Auto-generated ID used when this API interacts with other Pleo APIs |
## Other Use Cases
For use cases beyond what's described in this guide, please fill in [this form](https://forms.gle/5CHPcM15qH9umGoTA) to let us know.
# Migrate to the Export API
Source: https://developers.pleo.io/deprecation/legacy-expense-api/migration-guide-expenses-api-to-export-api
How to migrate an existing Expenses API integration to the Export API.
This guide is for partners who used the [Expenses API](/reference/pleo-deprecated/expenses/get-an-expense-for-a-company) to build custom export integrations to accounting systems. If you were fetching expense data for other workflows, see [Migrate to the Accounting Entries API](/deprecation/legacy-expense-api/migrate-to-accounting-entries-api) instead.
## New to the Export API?
End-to-end walkthrough of the Export API integration model.
Rules and requirements for each part of an export integration.
How to connect Pleo to route exports to your custom integration.
End-to-end OAuth 2.0 workflow guide for Pleo integrations.
How to configure Postman to authenticate using OAuth and call the Pleo API.
End-to-end guide for authenticating with standalone API keys.
How to make API calls using standalone API keys in Postman.
## What Changes in Your Integration
The table below gives a quick overview of how each step maps from the Expenses API to the Export API.
| Step | Before (Expenses API) | After (Export API) |
| :----------------------------------------------- | :------------------------------------------------ | :------------------------------------------------------------------------------ |
| [Detect new exports](#detecting-new-exports) | Poll `GET /v1/expenses?status=QUEUED` | Listen for `v1.export.job-created` webhook |
| [Start a job](#starting-a-job) | Not applicable | `POST /v3/export-job-events` with `"event": "started"` |
| [Fetch items](#fetching-export-items) | `GET /v1/expenses?status=QUEUED` | `GET /v3/export-jobs/{jobId}/items` |
| [Process export items](#processing-export-items) | At integration's discretion | Integration design guidelines and workflow guide |
| [Update item status](#updating-item-status) | `PUT /v1/expenses` with `{"status": "EXPORTING"}` | `PUT /v3/export-jobs/{jobId}/items` with granular statuses |
| [Complete the export](#completing-the-export) | `PUT /v1/expenses` with `{"status": "EXPORTED"}` | Update item statuses + `POST /v3/export-job-events` with `"event": "completed"` |
The following sections cover each change in detail.
### Detecting New Exports
**Before:**
```http theme={null}
GET /v1/expenses?status=QUEUED
```
**After:** Listen for the `v1.export.job-created` [webhook event](/reference/overview-webhooks).
How to Detect Available Export Jobs
### Starting a Job
**Before:** Not applicable.
**After:** Create a `started` event to claim the job before processing.
```http theme={null}
POST https://external.pleo.io/v3/export-job-events
Content-Type: application/json
{
"event": "started",
"jobId": "[JOB_ID]"
}
```
How to Start the Export Job
### Fetching Export Items
**Before:**
```http theme={null}
GET https://openapi.pleo.io/v1/expenses?status=QUEUED
```
**After:**
```http theme={null}
GET https://external.pleo.io/v3/export-jobs/[JOB_ID]/items
```
How to Retrieve Export Job Items
### Processing Export Items
**Before:** At integration's discretion.
**After:** Pleo provides integration design guidelines and a dedicated workflow guide covering bookkeeping methods, accounts mapping, data mapping, attachments, and posting behaviour.
AS/ERP Integration Design
AS/ERP Processing Workflow Guide
### Updating Item Status
**Before:**
```http theme={null}
PUT https://openapi.pleo.io/v1/expenses
Content-Type: application/json
{"status": "EXPORTING"}
```
**After:** Update individual items with granular statuses (`in_progress`, `successful`, `failed`, or `abandoned`).
```http theme={null}
PUT https://external.pleo.io/v3/export-jobs/[JOB_ID]/items
Content-Type: application/json
[
{"accountingEntryId": "[ITEM_ID]", "status": "in_progress"}
]
```
How to Update Export Items
### Completing the Export
**Before:**
```http theme={null}
PUT https://openapi.pleo.io/v1/expenses
Content-Type: application/json
{"status": "EXPORTED"}
```
**After:** Update item statuses to their final state, then create a `completed` event.
```http theme={null}
PUT https://external.pleo.io/v3/export-jobs/[JOB_ID]/items
Content-Type: application/json
[
{
"accountingEntryId": "[ITEM_ID]",
"status": "successful",
"exportedAt": "2025-07-31T12:48:52.236Z",
"externalId": "[ENTRY_ID_IN_YOUR_ACCOUNTING_SYSTEM]",
"externalUrl": "[ENTRY_URL_IN_YOUR_ACCOUNTING_SYSTEM]"
}
]
```
```http theme={null}
POST https://external.pleo.io/v3/export-job-events
Content-Type: application/json
{
"event": "completed",
"jobId": "[JOB_ID]"
}
```
How to Update and Complete Export Jobs
## Data Model Changes
The table below maps fields from the `Expense` model to their equivalents in the `Export Item` model.
* `—` : field is not available in the Export Item model
* `*` : data is the same but represented differently. See the [Export Item API reference](/reference/export-api/export-api-v3/get-export-job-items) for details.
* `**` : resolved entity relationship: the associated data is included directly, removing the need for additional API calls
| `Expense` field | `Export Item` equivalent |
| :---------------------------- | :------------------------------------------------- |
| id | accountingEntryId |
| employeeId | user.id |
| employeeCode | user.code |
| departmentId | team.id |
| performedAt | date |
| amountOriginal | — |
| amountSettled | amount **\*** |
| note | note |
| type | type **\*** |
| accountId | accountingEntryLines\[].account.id |
| taxCodeId | accountingEntryLines\[].tax.id |
| createdAt | date **\*\*** |
| updatedAt | date **\*\*** |
| deletedAt | — |
| tagIds\[].id | accountingEntryLines\[].tags\[].id |
| tagIds\[].tagGroupId | accountingEntryLines\[].tags\[].groupCode **\*\*** |
| receiptIds\[] | files\[] **\*\*** |
| settledExpenseIds\[] | additionalInformation.reconciledEntries\[] |
| status | — |
| supplier.CIF | supplier.taxIdentifier |
| supplier.documentNumber | — |
| cardTransaction.state | — |
| cardTransaction.authorizedAt | — |
| cardTransaction.settledAt | date |
| cardTransaction.reversedAt | — |
| cardTransaction.merchant.id | vendor.id |
| cardTransaction.merchant.name | vendor.name |
| lines\[].amountSettled | accountingEntryLines\[].lineAmount |
| lines\[].accountId | accountingEntryLines\[].account.id |
| lines\[].taxCodeId | accountingEntryLines\[].tax.id |
| lines\[].tagIds\[] | accountingEntryLines\[].tags\[].id |
Data Mapping Integration Design
# Overview
Source: https://developers.pleo.io/deprecation/legacy-expense-api/overview
The Pleo Expense API has been deprecated. Choose a migration path based on your use case.
**Expenses** in the Pleo API are now called **Accounting Entries**. You will see these two terms used interchangeably across this documentation.
For partners building custom integrations to export expenses to accounting systems not natively supported by Pleo.
For partners fetching expense data to build custom workflows such as dashboards, reports, or third-party syncs.
# Overview
Source: https://developers.pleo.io/deprecation/migration-guides
Guides to help you migrate from legacy Pleo APIs to their current replacements.
As legacy Pleo APIs reach end of life, migration guides will be published here to help you transition to the current APIs. Each guide covers the available migration pathways and what to expect during the process.
How to migrate from the legacy Expenses API to the Accounting Entries API, with two pathways depending on your use case.
More guides will be added as further legacy APIs reach end of life. End-of-life dates will be communicated in advance so you can plan your migration accordingly. Pleo Support will be on hand to answer any questions when that time comes.
# API Deprecation Notice
Source: https://developers.pleo.io/deprecation/overview
As part of our ongoing efforts to improve and innovate our product offerings, we have decided to deprecate a number of API endpoints from Pleo's API (now referred to collectively as the Legacy API). The legacy methods will be replaced by new methods over the coming months.
We understand that this change may impact your current workflow, and we want to assure you that we are committed to providing the support you need during this transition. Here are the key details and dates to keep in mind:
* **Deprecation Date:** Effective immediately, the Legacy API is considered deprecated. This means that while the API will remain operational until the end-of-life date, no new features will be added, and only critical security updates and bug fixes will be provided.
* **End-of-Life Date:** After this date, the API endpoints will no longer respond to requests.
* **Migration Support:** Once migration guides are available, they will be communicated alongside the end-of-life date so you can plan accordingly. Pleo Support will be on hand to answer any questions. Further information on the migration path for each use case is provided below.
* **Action Required:** If you are currently using the Legacy API, we recommend beginning the process of migrating your systems as soon as possible. Please review any integrations or applications that depend on the Legacy API and plan accordingly.
## Migration Plans
If your implementation of the Pleo API is for any of the following use cases, find more information about how these functions will be supported going forward.
### Accounting Export
Exporting Pleo expenses into your accounting or ERP system.
| Description | Legacy API | New API | Availability |
| :-------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------- | :----------- |
| Exporting accounting data from Pleo into your accounting or ERP system, including tax codes and receipts. | [Expense API](/reference/pleo-deprecated/expenses/get-an-expense-for-a-company) [Tax Codes API](/reference/pleo-deprecated/tax-codes/get-a-tax-code-for-a-company) [Receipt API](/reference/pleo-deprecated/receipts/get-expense-receipts) | [Export API](/reference/export-api-overview) | Available |
| Syncing cost centers/dimensions between Pleo and the ERP system | [Legacy Tag Groups API](/reference/pleo-deprecated/tag-groups/get-all-tag-groups-belonging-to-the-specified-company) | [Tag Group API](/reference/tags-api/tag-groups/returns-all-tag-groups-for-given-company) | Available |
| Setup of Categories and Accounts | [Legacy Accounts API](/reference/pleo-deprecated/accounts/get-an-account-for-a-company) | [Chart of Accounts API](/reference/accounts/chart-of-accounts-api-overview) | Available |
| Setup of Taxes | [Legacy Tax Codes API](/reference/pleo-deprecated/tax-codes/get-a-tax-code-for-a-company) | [Tax Code API](/reference/tax-codes/create-a-new-tax-code) | Available |
### User Management
Synchonising with your HR system to automate the adding or removal of user from Pleo
| Description | Legacy API | New API | Availability |
| :--------------------------- | :--------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------- |
| Add, update or delete a user | [Employee API](/reference/pleo-deprecated/employees/get-an-employee-for-a-company) | SCIM API | Q3 2026 |
| Teams and review structure | [Teams API](/reference/pleo-deprecated/teams/create-a-team-for-a-company) | [Companies API](/reference/companies/companies-api-overview) and [Employees API](/reference/employees/employees-api-overview) (company and employee info only) | Available |
### Project Management
Ensuring that cost centres within your project management software are represented in Pleo to easily reconcile expenses.
| Description | Legacy API | New API | Availability |
| :--------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------ |
| Synchronise expenses with your project management software | [Expense API](/reference/pleo-deprecated/expenses/get-an-expense-for-a-company) | Accounting Entries API or extended Export API | Available See [migration guides](/deprecation/legacy-expense-api/overview) |
| Synchronise Pleo Tags with cost centers | [Legacy Tag Groups API](/reference/pleo-deprecated/tag-groups/get-all-tag-groups-belonging-to-the-specified-company) | [Tag Groups API](/reference/tags-api/tag-groups/returns-all-tag-groups-for-given-company) | Available |
### Company Balance
| Description | Legacy API | New API | Availability |
| :--------------------------------- | :------------------------------------------------------------------------------------ | :------ | :----------- |
| Get the wallet balance for account | [Company Balance API](/reference/pleo-deprecated/company/get-company-account-balance) | TBD | Q3 2026 |
If you are customer, please direct any questions to your support contact. If you are a partner, you can email [api@pleo.io](mailto:api@pleo.io).
# Access Tokens Overview
Source: https://developers.pleo.io/docs/access-tokens
[Tokens Overview](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview)
In OAuth 2.0, access tokens are opaque strings that allow a client to access protected resources.
The exact format of access and refresh tokens is left out of scope of OAuth 2.0 specification, allowing the authorisation server to use the representation that fits their needs the best. Hence, the client must not assume any specific implementation and should treat access and refresh tokens as opaque bits of information.
* The validity of an access token is mentioned in the access token response.
* The validity of a refresh token is not defined, but it is valid for at least **60 days**. However, it cannot be guaranteed whether the refresh token would be valid after 60 days.
* If you are using an expired refresh token to get a new access token, all active refresh tokens are invalidated to prevent any replay attack.
The client might store the access tokens obtained from the authorisation server, and supply them in requests to resource servers, **but they must not peek inside the text content of the tokens**.
Access tokens are opaque. Do not rely on the content of access and refresh tokens in the integration of your client application with Pleo. Their format is not guaranteed and can be changed without notice. This can break the interoperability of your application with Pleo.
# Frequently Asked Questions
1. **How long is the access token valid**? The validity of an access token is specified in the `expires_in` parameter of the response.
2. **What should you do if the access token has expired**? The client sends a new access token request using the refresh token grant. The Pleo authorisation server provides a new access token and a new refresh token.
3. **After receiving the new access token and refresh token, are the old access token and refresh token valid?** No, you must discard the old access token and the refresh token.
4. **How long is a refresh token valid?** The validity of a refresh token is not defined. It is valid for at least 60 days. However, it cannot be guaranteed whether the refresh token would be valid after 60 days.
5. **What should you do if the refresh token has expired?** You must reinitiate a new OAuth 2.0 flow with an authorisation request.
# Access Tokens Introspection
Source: https://developers.pleo.io/docs/access-tokens-introspection
[Access Token Introspection](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-introspection)
The client might use the token introspection endpoint to to obtain information about access tokens. Clients can use this endpoint to check the validity of access tokens and find out other information, such as which resource and what scopes are associated with the token.
| Environment | Token Introspection Endpoint URI |
| :---------- | :---------------------------------------------------- |
| Staging | `https://auth.staging.pleo.io/oauth/token/introspect` |
| Production | `https://auth.pleo.io/oauth/token/introspect` |
### Introspection Request
Token introspection endpoint uses basic client authentication, using client identifier and client secret.
Client calls the introspection endpoint using **HTTP** `POST` request, with parameters sent as `application/x-www-form-urlencoded` data.
| Parameter | Description |
| :---------------- | :-------------------------------------------------------------------------------------------------------------------- |
| `token` | \[REQUIRED] The string value of the token. |
| `token_type_hint` | \[OPTIONAL] A hint about the type of the token submitted for introspection, either `access_token` or `refresh_token`. |
### Introspection Response
The server responds with a JSON object in `application/json` format with the following top-level members.
| Member | Description |
| :---------------------------------- | :------------------------------------------------------------------------------------------------------------------ |
| `active` | Boolean indicator of whether or not the presented token is currently active. |
| `sub` | ID of a resource that this access token grants access to. |
| `exp` | UNIX timestamp indicating when this token would expire. |
| `iat` | UNIX timestamp indicating when this token was originally issued. |
| `client_id` | Client identifier for the OAuth 2.0 client that requested this token. |
| `aud` | Service-specific string identifier or list of string identifiers representing the intended audience for this token. |
| `iss` | String representing the issuer of this token. |
| `jti` | String identifier for the token. |
| `urn:pleo:params:oauth:subject_urn` | Uniform Resource Name (URN) of a resource that this access token grants access to. |
### Example
In this example, a client with a client identifier `s6BhdRkqt3` and a client secret `gX1fBat3bV` issues a token introspection request to introspect an access token with the value of `mF_9.B5f-4.1JqM`.
```bash theme={null}
POST /oauth/token/introspect HTTP/1.1
Host: auth.staging.pleo.io
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
token=mF_9.B5f-4.1JqM
```
**Introspection response**:
```bash theme={null}
HTTP/1.1 200 OK
Content-Type: application/json
{
"active": true,
"client_id": "s6BhdRkqt3",
"sub": "b6e0abaf-0c69-4443-b59b-908cb6aabcce",
"urn:pleo:params:oauth:subject_urn": "urn:pleo:company:b6e0abaf-0c69-4443-b59b-908cb6aabcce",
"aud": "https://external.staging.pleo.io",
"iss": "pleo.staging",
"iat": 1720706356,
"exp": 1720707256,
}
```
# Accounting Entries Export Terms
Source: https://developers.pleo.io/docs/accounting-entries-export-terms
Terms used when exporting accounting entries from Pleo to an external ERP or accounting system.
An accounting entry that has been transformed into an exportable entry. The required accounting and bookkeeping information is aggregated from an accounting entry to become an export item.
The task that manages the export workflow, comprising the export items selected for export from Pleo to an external ERP or accounting system.
Events that cause a change in the status of an export job. When an event is triggered, the status of the corresponding export job is updated. Returned in the `status` parameter of the export-jobs endpoint. For example, when an export job fails, an event is triggered with a `failed` value; the reason for failure is included to help diagnose and resolve the issue.
Tracks the export progress of a specific accounting entry within an export job.
## Related Reading
* [How to Retrieve Export Job Items for Processing](/docs/current/how-tos/accounting-integrations/how-to-retrieve-export-job-items-for-as-erp-processing)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Integration: Quickstart](/docs/current/getting-started/accounting-integrations-quickstart)
# API Keys Overview
Source: https://developers.pleo.io/docs/api-keys
* [Standalone API Keys Overview](/docs/current/authentication/standalone-api-keys-overview)
* [Integrated API Keys Overview](/docs/current/authentication/integrated-api-keys-overview)
Another form of authorisation that Pleo supports is API keys and this form of authorisation is applicable, when:
* You are integrating Pleo with another application that does not support OAuth 2.0, for example, the applications that are deployed on-premises and these applications would also be included in the list of integration offerings that Pleo supports.
* You are a customer (company or organisation) interested in building a custom or bespoke integration with Pleo or temporarily ingest data from Pleo.
You cannot use API keys to access the Legacy APIs; you must generate API tokens to use the legacy API endpoints.
You can generate two types of API keys:
* [Standalone API keys](/docs/for-partners): Generate **standalone API keys** when you want to integrate your Pleo account with bespoke applications.
* [Integrated API keys](/docs/integrated-api-key-generation): During the installation process of connecting your Pleo account with an application that is already existing in the list of supported integrations, the API keys that you generate are **integrated API keys**.
# Authenticating Using API Keys
Source: https://developers.pleo.io/docs/authenticating-using-API-keys
* [Standalone API Keys Overview](/docs/current/authentication/standalone-api-keys-overview)
* [Standalone API Key Workflow Guide](/docs/current/guides/standalone-api-keys-workflow-guide)
* [How to Make an API Call Using a Standalone API Key](/docs/current/how-tos/api-keys/how-to-make-an-api-call-using-standalone-api-keys)
* [How to Make an API Call Using a Standalone API Key (Postman)](/docs/current/how-tos/api-keys/how-to-make-an-api-call-using-standalone-api-keys-postman)
## Using API Keys in Curl
Specify the API key in the `-u` or `--user` flag. The password is an empty string.
Here is an example:
```bash theme={null}
curl --request GET -sL \\
--url ''\\
-u 'enter the API key here'
```
## Using API Keys in a Graphical User Inteface (GUI) Tool
Here, we are using **Postman** for reference:
1. In the `Authorization` header of the request, select **Basic Auth** as the **Auth Type**.
2. In the `Username`, enter the API key.
3. In the `Password`, do not enter any value.
# Introduction
Source: https://developers.pleo.io/docs/authentication-authorisation-introduction
* **[OAuth 2.0 Access to Staging Workflow Guide](/docs/current/guides/environment-access/staging-oauth-workflow)**
* **[Environments](/docs/current/getting-started/testing-and-environments)**
The target audience for this page is the following users:
* Developer partners commissioned by Pleo.
* Customers (Companies or Organisations willing to integrate with Pleo).
Here is a brief outline of the entire authorisation process:
1. **Client credentials for authorisation**: Get credentials for authorisation. We support the following authorisation methods:
* [OAuth 2.0](/docs/oauth-20)
* [API Keys](/docs/api-keys)
The Pleo APIs are currently available through our Early Access Programme. We're eager to collaborate with independent developer partners. If you're interested in exploring how you can integrate with Pleo, please submit your request through this [form](https://docs.google.com/forms/d/e/1FAIpQLScJxF9CynyJ6stm8FNOK21wgLYBkgUJL3pnUsHvI5hNIMNtFA/viewform).
2. **Test Account**: Generate a test account. This is provided to you during the onboarding process.
3. **Use Pleo**: Start your journey with Pleo APIs.
# Overview
Source: https://developers.pleo.io/docs/build-with-pleo
* **[OAuth 2.0 Access to Staging Workflow Guide](/docs/current/guides/environment-access/staging-oauth-workflow)**
* **[Environments](/docs/current/getting-started/testing-and-environments)**
Collaboration with Pleo as a Developer Partner empowers you to connect your customers with a smart, sophisticated spend management platform that handles all aspects of business spending.
## How to Connect with Pleo?
The process is simple:
1. **Submit access request**: Fill in this form.
2. **Provide Client registration details**: Once your request is approved, we provide a client registration form where you must provide details of the integration you are building with Pleo (for example, name of the third-party application that you are trying to integrate with Pleo and the preferred authentication method).
3. **Access to Pleo APIs**: Based on the details you have provided, we grant you access to the Pleo APIs and provide credentials for authentication. Also, the application is listed as part of the App Marketplace.
4. **Build your integration**: Integrate Pleo with the third-party application.
## Apply for Production Access
Once you have built and tested your integration in the staging environment, you can request access to our production environment. To get access to the production environment, contact your integration partner.
# OAuth 2.0 Client Configuration
Source: https://developers.pleo.io/docs/client-configuration
Configuring your application to work with Pleo authorization server
* **[OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)**
After registering the OAuth 2.0 client, configure it using the information provided below.
| Parameter | Value |
| :--------------------- | :------------------------------------------------------------------------------------ |
| Authorization Endpoint | `{AUTHORIZATION_SERVER_URL}/oauth/authorize` |
| Token Endpoint | `{AUTHORIZATION_SERVER_URL}/oauth/token` |
| Grant Type | Authorization code grant, with PKCE extension. |
| Client Credentials | Client identifier and client secret of your client. |
| Client Authentication | Only `client_secret_basic` client authentication method is supported. |
| Redirect URI | URI of a redirection endpoint used by your client. |
| PKCE | Required, if supported by the client. Only `S256` code challenge method is supported. |
Exact mapping between these parameters and the configuration options provided by OAuth 2.0 client implementation, depend on the choice of the the software. Consult the documentation provided by your chosen implementation of OAuth 2.0 client.
## Authorisation Server URL
When configuring authorisation and token endpoints, substitute `{AUTHORIZATION_SERVER_URL}` with the base URL of the authorisation server in the environment that you’re developing for.
| Environment | Authorization Server URL |
| :---------- | :----------------------------- |
| Staging | `https://auth.staging.pleo.io` |
| Production | `https://auth.pleo.io` |
# Configuring Categories
Source: https://developers.pleo.io/docs/configuring-categories
* **[Classifying Expenses: Tags, Categories, and Tax Codes](/docs/current/platform/accounting-integrations/expense-classification-overview)**
* **[Chart of Accounts Sync Overview](/docs/current/platform/accounting-integrations/imports/accounts/accounts-sync-overview)**
* **[How to Set Up Your Categories](https://help.pleo.io/en/support/solutions/articles/103000254857-how-to-set-up-your-categories)** (manual setup in the Pleo Web App, includes video)
Categories imply the different types of accounting entries.
Categories in Pleo = Chart of Accounts in an ERP/Accounting software
So, Pleo represents the different accounts from chart of accounts as **Categories**. For example, **meals and drinks**, **IT hardware equipment**, and **other office supplies**. Each expense is incurred for a specific purpose and the purposes are classified and represented as **Categories**.
A few important characteristics of **Categories**:
* When you are creating a new category, you can mention a specific tax code for the category.
* Most accounting entries must have a category assigned. This helps in understanding why the expense is incurred and is useful in categorisation of entries. For example, an out of pocket expense recorded in Pleo is related with **Meals & drinks** - **Lunch allowance**; only exceptions are **Wallet Top Ups** and **Wallet Unloads** where assignment of categories is not mandatory.
* A **Category Group** is like an umbrella term that includes individual categories. For example, a category group is **Meals & drinks** that includes **Lunch allowance**, **Meals & drinks while traveling**, and **Meals & drinks with customers**; each additional value is an individual category.
# Configuring Customer Account in Pleo
Source: https://developers.pleo.io/docs/configuring-customer-account-in-pleo
* **[Classifying Expenses: Tags, Categories, and Tax Codes](/docs/current/platform/accounting-integrations/expense-classification-overview)**
For correct representation of accounting data and successful export to any ERP/accounting system, you must set up a few configurations in Pleo.
If there are specific configurations relevant to a certain accounting system, you must configure them during the integration set up.
## Configuring Accounting Settings
After the integration with an ERP/accounting system is complete, set up the following configurations in the specific order mentioned:
1. **Tax Codes**: For more information on tax codes, see [here](/docs/configuring-tax-codes).
2. **Categories**: For more information on categories, see [here](/docs/configuring-categories).
3. **Tags**: For more information on tags, see [here](/docs/configuring-tags).
# Configuring Tags
Source: https://developers.pleo.io/docs/configuring-tags
* **[Classifying Expenses: Tags, Categories, and Tax Codes](/docs/current/platform/accounting-integrations/expense-classification-overview)**
* **[Tags Sync Overview](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview)**
* **[How to Set Up Your Tags](https://help.pleo.io/en/support/solutions/articles/103000254859-how-to-set-up-your-tags)** (manual setup in the Pleo Web App, includes video)
Tags imply cost centres. So when you assign tags to an accounting entry, it helps in allocating the correct cost centre to an accounting entry. For example, an expense of the **Flight** category is recorded in Pleo. This specific accounting entry is assigned the **Business Trip** tag. Assignment of tags helps in assessment of cost incurred for specific reasons.
A few important characteristics of **Tags**:
* A **Tag Group** is like a collection of tags. For example, a tag group is a **Department** and it includes **Sales**, **Marketing**, and **IT**. Each individual value is a tag; hence, **Sales**, **Marketing**, or **IT** is a tag indicating a specific department.
* Tag Group = Dimension Group
* Tag = Dimension value
Hence, each tag is like adding a dimension to the Tag Group/Dimension Group. And the display order of the dimension values are indicated by the number assigned to each dimension value.
* Each tag group and tag have specific IDs assigned to them in Pleo for easy identification.
* You can either create new tags in Pleo or you can import tags from the ERP/accounting system that you have integrated with. In addition, you can also import tags from an excel spreadsheet, enabling customers to track the types and volumes of expenses recorded in Pleo based on the tags assignment.
* You can assign a maximum of five tags to an accounting entry.
You can set up tags either using the Tags API or in the Pleo application:
* To set up tags using the Tags API, see the [Tags API](/reference/tags-overview).
* To set up tags in the Pleo application, perform the following in the **Settings** > **Accounting** > **Tags** tab:
* Click **Create tag group**.
* Enter a name for your tag group and then click **Create tag group**.
* Under **Tags**, specify the individual dimension values that you would like to add. For example, **Workplace Management** tag group might contain location specific tags as relevant for the customer's business operations.
# Configuring Tax Codes
Source: https://developers.pleo.io/docs/configuring-tax-codes
* **[Classifying Expenses: Tags, Categories, and Tax Codes](/docs/current/platform/accounting-integrations/expense-classification-overview)**
* **[Tax Sync Overview](/docs/current/platform/accounting-integrations/imports/tax-codes/tax-sync-overview)**
* **[How to Set Up Your Tax Codes](https://help.pleo.io/en/support/solutions/articles/103000254846-how-to-set-up-your-tax-codes)** (manual setup in the Pleo Web App)
Tax codes imply the Value Added Tax (VAT) rate applied to an expense.
Hence, you must specify the appropriate tax codes that are relevant for your business- this helps to apply the correct tax code to an expense recorded in Pleo.
* To set up tax codes using the **Taxes API**, see the [Tax Code API](/reference/tax-codes-overview).
* To set up tax codes in the Pleo application, mention the following details in the **Settings** > **Accounting** > **Setup** tab:
| Columns | Description |
| :------- | :------------------------------------------------------------------------------------- |
| Type | Defines the specific type allocated to the tax. |
| Name | A specific name for the tax code. |
| Code | An internal identifier of the ERP/accounting system assigned to the specific tax type. |
| Tax rate | The percentage of tax applied to an expense with this specific tax code. |
# Connecting Pleo with ERP/Accounting System
Source: https://developers.pleo.io/docs/connecting-pleo-with-erpaccounting-system
* **[Accounting Integration Deployment Decisions](/docs/current/getting-started/accounting-integrations-deployment-decisions)**
* **[How To Connect Pleo to a Pre-Built ERP or Accounting System](/docs/current/how-tos/how-to-connect-pleo-to-pre-built-as-systems)**
The following steps describe a typical workflow of connecting Pleo with an ERP or accounting system:
1. In the Pleo application, click **Settings**.
2. Click **Accounting**.
3. Search for the ERP or the accounting system that you want to integrate with.
4. Click the specific ERP or the accounting system and then click **Connect**.
Based on the ERP or the accounting system that you select, the connection procedure varies. For example, if you want to integrate Pleo with Xero, the log in page to Xero appears.
You can also create a custom integration with Pleo. In addition, you can also create a custom format or use a Pleo format to download the accounting entries recorded in Pleo.
## Deployment of Pleo Integration with ERP/Accounting System
You have the following options for the integration deployment:
* **Module or on-premises**:
Some ERP and accounting systems provide a platform for deployment of the integration in their environment. For example, NetSuite and SAP B1. This method has the following benefits:
* The integration utilises the ERP/accounting system provided platform, ensuring higher stability and connectivity.
* The integration could use the platform tech stack for building the configuration procedure.
* There is only a single step of authorisation required to access data from Pleo.
This deployment option might not be available for all third-party integrations and could incur additional cost. Please check with your partner manager or customer success team for more details.
* **Deployment on Cloud Platform**:
The integration is deployed on a cloud platform. Hence, when you connect Pleo with the third-party application, the following occurs:
The cloud platform specific configuration page appears.
Complete the integration process here - authenticate and authorise the integration for data transition between Pleo and the ERP/accounting system.
We have partnered with Pandium for deployment of Pleo integrations with third-party applications.
# API Base URLs
Source: https://developers.pleo.io/docs/current/authentication/api-base-urls
The **API Base URL** determines which environment your API requests are sent to.
## How Base URLs Are Constructed
`Base URL + Endpoint Path = Final Request URL`
| Environment | Base URL | Example Endpoint | Final Request URL |
| ----------- | ---------------------------------- | ---------------- | ----------------------------------------------- |
| Staging | `https://external.staging.pleo.io` | `/v2/employees` | `https://external.staging.pleo.io/v2/employees` |
| Production | `https://external.pleo.io` | `/v2/employees` | `https://external.pleo.io/v2/employees` |
See: [API Reference pages](/reference/employees/search-for-employees) for all API endpoints.
## Choosing an Environment
We offer **Staging** and **Production** [environments](/docs/current/getting-started/testing-and-environments).
* **Staging:** safe testing, experiments, pre-production validation
* **Production:** live data, ready integrations
## Authentication
Pleo APIs support multiple authentication methods depending on the integration type.
| Method | Typical Use |
| ------------------- | ---------------------------------------------- |
| OAuth 2.0 | Partner integrations |
| Integrated API Keys | Approved exception only |
| Standalone API Keys | Restricted integrations and specific use cases |
All authentication methods work with the **External API base URLs** shown above.
### OAuth 2.0 (Bearer Token)
Partner integrations must use **OAuth 2.0**.
API requests must include the following header:
```text theme={null}
Authorization: Bearer
```
Follow the [**OAuth 2.0 Setup Workflow Guide (Manual Token Lifecycle)**](/docs/current/guides/oauth-workflow-guide) to generate an access token.
### Integrated API Keys
Upon [approval](/docs/current/getting-started/developer-partnership-programme#integrated-api-keys-approved-exception-only) from Pleo, some partner integrations may be permitted to use Integrated API Keys.
### Standalone API Keys
Standalone API Keys have [**restricted availability**](/docs/current/authentication/standalone-api-keys-overview) and are only supported by Pleo's [NEW](#faqs) **External APIs**.
If enabled for your account, follow the **Standalone API Key Workflow Guide** to create a key and make your first API request.
Authentication uses **Basic HTTP authentication**:
* API key as the username
* Empty password
* Credentials are automatically Base64 encoded
***
## Rate Limits
All Pleo APIs have a rate limit of 600 requests per minute unless stated otherwise in the API Reference.
***
## What Comes Next?
### Review Integration Requirements
* Integration Design for [OAuth 2.0 Overview](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview)
* Integration Design for [Standalone API Keys](/docs/current/integration-design/auth/api-keys/integration-design-for-standalone-api-keys)
### Setup Authentication
* [OAuth 2.0 Access to Staging Workflow Guide](/docs/current/guides/environment-access/staging-oauth-workflow)
* [OAuth 2.0 Setup Workflow Guide (Manual Token Lifecycle)](/docs/current/guides/oauth-workflow-guide)
* [OAuth 2.0 Setup with Postman](/docs/current/guides/oauth-workflow-guide-postman)
* [Standalone API Key Workflow Guide](/docs/current/guides/standalone-api-keys-workflow-guide)
***
## FAQs
**Legacy APIs (OpenAPI)**
* **Base URL:** `https://openapi.pleo.io`
* **Authentication:** API tokens (legacy tokens)
* **Availability:** Intended for existing/legacy use cases. Access can depend on your account setup and entitlements.
* **Lifecycle:** Deprecated. Pleo is working on a deprecation timeline and migration plan.
**New APIs (External API)**
* **Base URL:** `https://external.pleo.io`
* **Authentication:** API keys (and other authentication methods supported by the platform, including [OAuth 2.0](/docs/current/authentication/oauth/oauth-overview), depending on the use case)
* **Availability:** Restricted access. [Documentation](/reference/api-introduction) may be publicly visible, but access is not broadly enabled by default. Standalone API keys require explicit enablement.
* **What to expect:** Newer platform surface and improved structure, but not guaranteed feature parity with legacy APIs yet.
**Important:**
* Legacy API tokens **will not work** on `external.pleo.io`
* New API keys **will not work** on `openapi.pleo.io`
For more context on Standalone API Keys and environment restrictions, see the [Standalone API Keys Overview](/docs/current/authentication/standalone-api-keys-overview) article.
***
# API Scopes
Source: https://developers.pleo.io/docs/current/authentication/api-scopes
API scopes define the permissions your application has when accessing Pleo APIs.
Scopes determine which resources your application can read or modify. Your application cannot access APIs or perform actions outside the scopes granted to it.
You request scopes when [registering your application](/docs/current/how-tos/oauth/how-to-register-an-oauth-client#technical-information) as part of Pleo's Technology Partner [Early Access Programme](/docs/current/getting-started/developer-partnership-programme) and during the [authorisation](/docs/current/how-tos/oauth/how-to-direct-users-to-the-authorisation-endpoint#3-build-the-authorisation-request) process.
## Scope format
Scopes use a resource and permission format:
`resource:permission`
Examples:
* export-jobs:write
* export-jobs:read
* users:read
## Available scopes by API
Each API defines its own set of scopes:
* [Exports API Scopes](/reference/export-api/export-api-scopes)
* [Tags API Scopes](/reference/scopes-2)
* [Chart of Accounts API Scopes](/reference/accounts/chart-of-accounts-api-scopes)
* [Vendors API Scopes](/reference/vendor/vendor-api-scopes)
* [Tax Code API Scopes](/reference/scopes-3)
* [Employees API Scopes](/reference/employees/employees-api-scopes)
* [Companies API Scopes](/reference/companies/companies-api-scopes)
## When scopes are evaluated
Scopes are enforced when your application makes API requests. If your access token does not include the required scope, the API will reject the request.
## Important
Ensure you request all scopes required for your integration. Missing scopes will prevent your application from accessing the corresponding API endpoints.
# Integrated API Keys Overview
Source: https://developers.pleo.io/docs/current/authentication/integrated-api-keys-overview
Integrated API Keys provide a **pre-scoped, company-level credential** for accounting or ERP integrations that **cannot support OAuth 2.0**.
They are generated by Pleo during the integration install flow and are designed for **per-customer, per-installation connections**. Unlike OAuth 2.0, there is **no redirect, auth code, or token lifecycle** to implement. The key is tied to the customer installation and [pre-configured scopes](/docs/current/authentication/api-scopes).
Integrated API Keys [**require approval**](/docs/current/getting-started/developer-partnership-programme#integrated-api-keys-approved-exception-only) from Pleo. They are typically used only for **accounting/ERP integrations** where OAuth 2.0 is not feasible.
Integrated API Keys are **restricted** and are **not self-service**. If you’re considering using them, note:
* Integrated API Keys are **granted case-by-case** during onboarding
* Only approved accounting or ERP integrations can use them
* Each key is **tied to a specific customer installation**; it is not a generic API key
## Concepts and Terminology
| **Concept** | **Term** | **Description** |
| ------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Company credential | **Integrated API Key** | A secret key generated during installation of a Pleo-approved integration, scoped to a single customer installation. Each customer receives a unique key. |
| Integration configuration | **Auth client / Marketplace entry** | Defines the scopes and permissions pre-configured into the key by Pleo. Managed internally by Pleo; no OAuth 2.0 client lifecycle exists. |
| Permission boundary | **Scoped access** | The set of [API actions](/docs/current/authentication/api-scopes) the integration is allowed to perform. Pre-configured at install time. |
| Company identity | **Company\_id** | Identifies the Pleo company whose data the integration can access. |
Integrated API Keys represent **direct, pre-approved access** for an integration to act on behalf of a customer installation. They are **per-installation credentials**, meaning each customer gets a unique key. Integrations can therefore support **multiple customers** by storing and managing one key per customer.
## When to Use Integrated API Keys
Use Integrated API Keys if:
* Your integration **cannot support OAuth 2.0**
* You are building an **approved accounting/ERP integration**
* You need **pre-configured permissions** without implementing token flows
* You are prepared to manage **per-customer keys** for a multi-customer integration
Otherwise, [OAuth 2.0](/docs/current/authentication/oauth/oauth-overview) is strongly recommended.
## How Integrated API Keys Work (Conceptually)
### Install-Scoped Access
* Each key is generated **per customer installation**
* The key is bound to that customer + integration context and **cannot be reused across other customers**
* Keys are generated **only in approved flows via the Pleo Web App**
### Pre-Scoped Permissions
* Permissions/scopes are **configured by Pleo during integration setup**
* The key can only perform operations allowed by these scopes
* Scope management is **internal to Pleo**, not editable by the integration
### Integration Validation
* Integrations must validate the key by making a **test API call**
* Upon successful validation, the integration stores the key securely and tracks the relevant `company_id` or other Pleo identifiers
* Integrations should support **storing multiple keys**, one per customer installation
## High-Level Workflow
### 1. Onboarding prerequisites (Integration Developer → Pleo)
1. **Request access** to build an accounting/ERP integration
2. **Confirm eligibility** for Integrated API Keys with Pleo
3. **Provide Marketplace integration details** (name, branding, support contacts, features)
4. **Pleo configures the integration** (“auth client”) with pre-scoped permissions
### 2. Customer install flow (Customer + Pleo)
1. Customer initiates installation via Pleo Marketplace or Accounting settings
2. Pleo presents an **Integrated API Key** step in the install modal
3. Pleo verifies the customer has the required permissions
4. If permitted, Pleo generates a **customer-specific key** and presents it to the customer
### 3. Customer transfers key to integration (Customer + Integration)
1. Customer copies the generated key
2. Integration provides a **secure input field** to paste and save the key
3. Integration must support **masking, secure storage, and future rotation per customer**
### 4. Integration validates and stores key (Integration + Pleo APIs)
1. Validate key with a test API call
2. On success:
* Save key securely
* Store relevant identifiers (company\_id)
* Show “Connected”
3. On failure:
* Show clear error
* Prompt for re-entry or support contact
### 5. Ongoing maintenance
* Customers may rotate or recreate keys
* Integration must support **key updates, re-validation, and rotation per customer installation**
## Implementation Checklist for External Developers
**Integration developer must implement:**
* Connect screen with:
* Secret API key input
* Save + Test connection button
* Clear error states
* Secure key storage and masking
* Key rotation / reconnect flow per customer
* Minimal health check API call for validation
**Pleo provides:**
* Key generation UI in install flow
* Scoped key permissions
* Record visibility under **Settings → API Keys** after install
## Security Responsibilities (High-Level)
* Treat Integrated API Keys as **secrets**
* Mask keys after save; avoid logs, analytics, or client-side exposure
* Rotate or replace compromised keys
* Validate that keys have the required scopes
* Manage keys **per customer installation**
## Integrated API Keys vs OAuth 2.0
| | **Integrated API Keys** | **OAuth 2.0** |
| -------------------------- | --------------------------------------------------------------- | ---------------------------- |
| Availability | Restricted / approved integrations only | Available |
| Access model | Company-scoped (per-customer installation) | User-delegated |
| Typical usage | Accounting / ERP multi-customer integrations | Multi-customer integrations |
| Customer consent | Implicit in install | Explicit consent required |
| Multi-customer support | Yes — one integration can serve multiple customer installations | Yes |
| Authentication | Static per-customer Integrated API key | Access tokens |
| Recommended for production | Approved exceptions | Default for all integrations |
## Typical Development Workflow
1. Onboard with Pleo and confirm Integrated API Key eligibility
2. Implement secure connect screen with key input
3. Validate key with a test API call
4. Support reconnect / rotation flows **per customer**
5. Optional: implement health check or minimal API calls to verify integration functionality
Implementation details for installation, activation, key updates, and error handling are covered in:
* [**Integration Design for Integrated API Keys**](/docs/current/integration-design/auth/integrated-api-keys/integration-design-for-integrated-api-keys)
* [**User Experience Guidelines for Integrated API Keys**](/docs/current/integration-design/auth/integrated-api-keys/integration-design-user-exp-guidelines-for-integrated-api-keys)
***
## What Comes Next?
* [**Integration Design for Integrated API Keys**](/docs/current/integration-design/auth/integrated-api-keys/integration-design-for-integrated-api-keys)
***
## Related Reading
* [**User Experience Guidelines for Integrated API Keys**](/docs/current/integration-design/auth/integrated-api-keys/integration-design-user-exp-guidelines-for-integrated-api-keys)
* [Security and Credential Management for Integrated API Keys](/docs/current/integration-design/auth/integrated-api-keys/integration-design-security-for-integrated-api-keys)
***
# OAuth 2.0 Overview
Source: https://developers.pleo.io/docs/current/authentication/oauth/oauth-overview
OAuth 2.0 is an industry-standard authorisation framework that allows **integrations** to access Pleo data securely on behalf of a customer, **without requiring the customer to share their login credentials**.
Instead of passwords, OAuth 2.0 uses **access tokens**, which grant limited, controlled access to specific resources and actions.
This allows integrations to operate securely while ensuring customers remain in control of their data.
OAuth 2.0 is the **recommended authorisation method for most Pleo integrations**.
Because OAuth 2.0 terminology can vary between platforms, the following definitions explain how these concepts are used within the Pleo Partner platform.
## Concepts and terminology
Before getting started, it helps to understand how key terms are used in Pleo's platform.
| **Concept** | **Term** | **Description** |
| -------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Partner-built system | **Integration** | The external system or service you build that connects to Pleo APIs (for example, an accounting platform or internal tool). |
| Registered identity | **OAuth 2.0 client** | The technical identity registered in Pleo that allows your integration to authenticate and request access tokens. |
| Marketplace product | **App / Application** | The user-facing representation of your integration shown during authorisation and within the Pleo Marketplace. |
Throughout this documentation, **integration** refers to your system that connects to Pleo, **OAuth 2.0 client** refers to the technical identity registered with Pleo, and **application** (or **app**) refers to the user-facing representation of your integration in the Pleo UI or Marketplace.
Understanding these distinctions will help you correctly interpret the instructions in the OAuth 2.0 documentation and avoid confusion between the system you build, its registered identity, and its user-facing representation.
## When to use OAuth 2.0
OAuth 2.0 is the required default authentication method for technology partners participating in the [Early Access Program](/docs/current/getting-started/developer-partnership-programme). You should use OAuth 2.0 if your integration needs to:
* Access Pleo APIs on behalf of a customer
* Allow customers to connect their Pleo account to your integration
* Access or modify customer data, such as expenses, employees, or accounting information
* Maintain secure, long-lived access without storing user credentials
OAuth 2.0 ensures customers explicitly grant permission and can revoke access at any time.
Alternative authentication methods, such as Integrated API keys, may be used in [limited scenarios](/docs/current/getting-started/developer-partnership-programme#authentication-requirements).
## How OAuth 2.0 protects customer data
OAuth 2.0 improves security by:
* Never exposing user passwords to third-party integrations
* Issuing **access tokens** with limited permissions
* Allowing integrations to request only the access they need using [API scopes](/docs/current/authentication/api-scopes)
* Allowing access to be revoked without affecting user login credentials
This ensures integrations operate within clearly defined permission boundaries.
## OAuth 2.0 roles (as used by Pleo)
OAuth 2.0 defines four roles involved in granting and using access:
* **Resource Owner**\
The Pleo customer who owns the data and grants access to it.
* **Client**\
The OAuth 2.0 client representing your integration when requesting access tokens.
* **Authorisation Server**\
Pleo’s service that authenticates users, collects consent, and issues access tokens.
* **Resource Server**\
Pleo APIs that store and provide access to protected data.
## What happens when a customer connects your integration
At a high level:
1. Your integration directs the customer to Pleo to approve access
2. The customer reviews and approves the requested permissions
3. Pleo issues access tokens to your integration
4. Your integration uses those tokens to access Pleo APIs securely
The customer can revoke access at any time.
## What Comes Next?
Once you decide that OAuth 2.0 is the right authorisation method for your integration, the next step is to design and implement it correctly.
See:
* [OAuth 2.0 Overview](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview) – Understand how OAuth 2.0 works at the implementation level
* [OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide) – Follow a step-by-step implementation guide
# Standalone API Keys Overview
Source: https://developers.pleo.io/docs/current/authentication/standalone-api-keys-overview
Standalone API Keys provide a simple way to authenticate requests to Pleo APIs using a **company-controlled credential** instead of delegated user authorisation.
A Standalone API Key is generated directly from the **Pleo Web App** and allows systems to access Pleo APIs for a **single organisation**, without requiring an OAuth 2.0 authorisation flow.
Standalone API Keys are primarily intended for **API exploration, internal tooling, and Pleo pre-approved single-company integrations**.
For most production integrations, especially those connecting multiple customers, **OAuth 2.0 remains the recommended authorisation method**.
Standalone API Keys are **restricted** and are **not available to most customers or partners**.
If you’re considering using them, note the following:
* Standalone API Keys are **not self-service**
* Access must be explicitly enabled by Pleo during onboarding
* Most customers currently use [**Legacy APIs**](#faqs), not the new External API platform
* Seeing this documentation **does not mean you have access**
* You must be an **admin** or **bookkeeper** to generate Standalone API Keys
If you do not see API Keys in the **Pleo Web App**, then Standalone API Key access has not been enabled for your organisation.
Standalone API Keys are **not available for customers using [Legacy APIs](#faqs)**.
**Support expectations:**
Standalone API Keys are provided only in approved scenarios. When access is granted:
* Pleo support is limited to information available in the public developer portal
* Custom implementation support is generally not provided by Pleo
* Integrations are expected to be self-managed by the customer/partner implementation and development teams
## Concepts and Terminology
Before getting started, it helps to understand how Standalone API Keys are used within the Pleo platform.
| **Concept** | **Term** | **Description** |
| ------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| Company credential | **Standalone API Key** | A secret key generated in the Pleo Web App used to authenticate API requests for a single company. |
| Environment | **Staging / Production** | The Pleo environment where the API Key is created and valid. Keys only work in the environment where they were generated. |
| Permission boundary | **API scopes** | Permissions assigned to a key that define which API actions it may perform. |
| Company identity | **Company\_id** | The Pleo company identity whose data the API Key can access. |
Unlike OAuth 2.0, Standalone API Keys do not represent a user or external application acting on behalf of multiple customers. Instead, they represent **direct access granted by a company to its own systems**.
## When to Use Standalone API Keys
Standalone API Keys are appropriate when API access is limited to a **single organisation** and user authorisation is not required.
Typical use cases include:
* Exploring or testing Pleo APIs during development
* Building internal tools or scripts
* Exporting data to internal systems or data warehouses
* One-off migrations or reporting workflows
* Approved integrations that cannot support OAuth 2.0 or Integrated API Keys (for example, legacy systems or restricted environments)
## When to Use OAuth 2.0 Instead
OAuth 2.0 should be used if your integration:
* Connects multiple Pleo customers
* Acts on behalf of users or external organisations
* Requires customer consent and revocable access
* Will be distributed as a partner or marketplace integration
OAuth 2.0 provides delegated, user-authorised access and is the default authentication method for most integrations.
See the [**OAuth 2.0 Overview**](/docs/current/authentication/oauth/oauth-overview) for details.
## How Standalone API Keys Work (Conceptually)
### Environment-Scoped Access
Each Standalone API Key is bound to the environment where it was created:
* A key created in **Staging** only works in Staging
* A key created in **Production** only works in Production
Keys cannot be reused across environments.
### Permission-Scoped Access
When creating a key, you select [**API scopes**](/docs/current/authentication/api-scopes) that define allowed actions.
For example:
* `users:read`
* `export-jobs:read`
* `export-jobs:write`
The key can only perform operations allowed by its assigned scopes.
### Company-Scoped Authentication
Standalone API Keys provide access to a **single Pleo company**.
API requests must include the correct `company_id`, ensuring requests operate within the intended organisational boundary.
This results in a **1-to-1 integration model** between a system and a Pleo organisation.
The same principle applies to [multi-entity](/docs/current/platform/multi-entity/multi-entity-and-api-scope) accounts.
Detailed guidance on API scopes, company scoping, and integration lifecycle is in [**Integration Design for Standalone API Keys**](/docs/current/integration-design/auth/api-keys/integration-design-for-standalone-api-keys).
## Security Responsibilities (High-Level)
Because Standalone API Keys act as secret credentials, your organisation is responsible for protecting them:
* Store API Keys securely and never expose them publicly
* Avoid embedding keys in client-side applications
* Use the minimum required API scopes
* Rotate or replace keys if compromised
For implementation best practices and lifecycle handling, see [**Security and Credential Management for Standalone API Keys**](/docs/current/integration-design/auth/api-keys/integration-design-security-for-standalone-api-keys).
## Standalone API Keys vs OAuth 2.0
| | **Standalone API Keys** | **OAuth 2.0** |
| --------------------------------------- | ----------------------- | --------------------- |
| Availability | Restricted | Available |
| Access model | Company-controlled | User-delegated |
| Typical usage | Internal or exploratory | Customer integrations |
| Customer consent | Not required | Required |
| Multi-customer support | No | Yes |
| Authentication | Static API key | Access tokens |
| Recommended for production integrations | Limited cases | Yes |
* **Multi-customer:** Multiple customers using your integration.
* **Multi-entity:** Your organisation has multiple companies.
## Typical Development Workflow
1. Explore APIs using Standalone API Keys
2. Prototype workflows and validate API usage
3. Implement Standalone API Keys for production integrations (where approved by Pleo)
4. Implement OAuth 2.0 for production integrations (recommended)
Implementation details for installation, activation, key updates, and error handling are covered in:
* [**Integration Design for Standalone API Keys**](/docs/current/integration-design/auth/api-keys/integration-design-for-standalone-api-keys)
* [**User Experience Guidelines for Standalone API Keys**](/docs/current/integration-design/auth/api-keys/integration-design-user-exp-guidelines-for-standalone-api-keys)
* [Security and Credential Management for Standalone API Keys](/docs/current/integration-design/auth/api-keys/integration-design-security-for-standalone-api-keys)
## What Comes Next?
* [**Standalone API Key Workflow Guide**](/docs/current/guides/standalone-api-keys-workflow-guide) – Generate a key and make your first API call
***
## Related Reading
* [**How to Generate an API Key**](/docs/current/how-tos/api-keys/how-to-generate-standalone-api-keys)
* [**OAuth 2.0 Overview**](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview) – Learn the recommended integration authentication model
***
## FAQs
**Legacy APIs (OpenAPI)**
* **Base URL:** `https://openapi.pleo.io`
* **Authentication:** API tokens (legacy tokens)
* **Availability:** Intended for existing/legacy use cases. Access can depend on your account setup and entitlements.
* **Lifecycle:** Deprecated. Pleo is working on a deprecation timeline and migration plan.
**New APIs (External API)**
* **Base URL:** `https://external.pleo.io`
* **Authentication:** API keys (and other authentication methods supported by the platform, including [OAuth 2.0](/docs/current/authentication/oauth/oauth-overview), depending on the use case)
* **Availability:** Restricted access. [Documentation](/reference/api-introduction) may be publicly visible, but access is not broadly enabled by default. Standalone API keys require explicit enablement.
* **What to expect:** Newer platform surface and improved structure, but not guaranteed feature parity with legacy APIs yet.
**Important:**
* Legacy API tokens **will not work** on `external.pleo.io`
* New API keys **will not work** on `openapi.pleo.io`
For more context on Standalone API Keys and environment restrictions, see the [Standalone API Keys Overview](/docs/current/authentication/standalone-api-keys-overview) article.
***
# Accounting Integration Deployment Decisions
Source: https://developers.pleo.io/docs/current/getting-started/accounting-integrations-deployment-decisions
This page explains **how Accounting integrations can be deployed**, including options for different ERP/accounting systems and integration partners. It highlights **OAuth 2.0 authorisation**, data flow, examples, and best practices for stable, secure operation.
## Overview
Accounting integrations transfer **accounting-ready data** from Pleo to external systems using **API-triggered export jobs**. Deployment depends on your ERP/accounting system capabilities and whether the integration is partner-hosted or run in-house.
All deployments rely on **OAuth 2.0** for secure authorisation when accessing Pleo data.
## Deployment Options
### Module / On-Premises
Some ERP/accounting systems allow the integration to run **within their own environment**. Examples include **NetSuite** and **SAP B1**.
**Benefits include:**
* Higher stability and tighter connectivity
* Ability to leverage the ERP/AS platform tech stack for configuration
* Typically requires a single **OAuth 2.0 authorisation step** to access Pleo data
**Notes:**
* Not all ERP/accounting systems support this deployment
* May require additional licensing, platform setup, or technical approval
* Suitable for organisations that prefer in-house control over integration execution
* Check with your partner manager or customer success team to confirm availability and any additional costs
### Cloud Platform
The integration runs on a **cloud platform or partner-hosted middleware**, such as integrations deployed by **Pandium**.
**Key workflow:**
1. A bookkeeper or automation in the Pleo Web App initiates an **export job**.
2. Immutable, accounting-ready **JSON records** are produced.
3. Your integration consumes the records programmatically via API, authenticated using **OAuth 2.0**.
**Benefits include:**
* Centralised management by partners or middleware providers
* Works well for complex multi-system workflows
* Enables near real-time responsiveness using **webhook notifications** or polling
**Notes:**
* Integration logic can be decoupled from the ERP/AS platform
* Webhooks notify the integration of new export jobs (`v1.export.job-created`) or master data changes (`v1.vendor.created`)
* Ensure partner agreements are in place for any partner-hosted deployments
## Considerations for Both Deployment Types
* Ensure OAuth 2.0 tokens are stored and refreshed securely
* Follow API rate limits and error handling guidelines
* Traceability and auditing of export jobs are critical
* Test in **Staging** before promoting to **Production**
For more on webhooks and event-driven notifications, see [Event-Driven Notifications (Webhooks)](/reference/webhooks/overview-webhooks).
***
## What Comes Next?
* Review your ERP/Accounting system capabilities and determine which deployment approach fits best
* Familiarise yourself with [Accounting Integrations](/docs/current/getting-started/accounting-integrations-overview) and the [**Exports Lifecycle**](/docs/current/platform/exports/lifecycle)
* Ensure team members responsible for the integration understand [OAuth 2.0](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview) flows and token handling
* Set up a [**Staging Environment**](/docs/current/guides/environment-access/staging-oauth-workflow) for testing before Production deployment
***
## Related Reading
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [How To Connect Pleo to a Pre-Built ERP or Accounting System](/docs/current/how-tos/how-to-connect-pleo-to-pre-built-as-systems)
***
# AS/ERP Integration: Overview
Source: https://developers.pleo.io/docs/current/getting-started/accounting-integrations-overview
This page provides a **high-level overview** of custom Accounting Integrations between Pleo and external Accounting Systems (AS) or Enterprise Resource Planning (ERP) systems.
It explains **what accounting integrations are**, **which integration levels are available**, and **where to go next** depending on your implementation needs.
## What Is an Accounting Integration?
An Accounting Integration allows an ERP or Accounting System to **receive accounting-ready data from Pleo** using Pleo APIs.
These integrations are typically used when:
* No pre-built integration exists
* Custom bookkeeping logic is required
* Advanced automation or synchronisation is needed
* The ERP system requires a bespoke deployment model
Accounting Integrations are **export-driven** and focus on moving expenses, attachments, and related accounting data from Pleo into an external system in a predictable and auditable way.
If your system is supported out of the box, you may prefer a [pre-built integration](/docs/current/how-tos/how-to-connect-pleo-to-pre-built-as-systems).
## Integration Levels
Accounting Integrations can be implemented at different **levels**, depending on which APIs are used and how much automation is required.
| Level | Export | Tags | Categories | Vendors | Corresponding APIs |
| :------ | :----- | :--- | :--------- | :------ | :---------------------------------------------------------------- |
| Level 1 | ✅ | | | | Export API |
| Level 2 | ✅ | ✅ | | | Export API + Tags API |
| Level 3 | ✅ | ✅ | ✅ | | Export API + Tags API + Accounts API + Tax Code API |
| Level 4 | ✅ | ✅ | ✅ | ✅ | Export API + Tags API + Accounts API + Tax Code API + Vendors API |
**How to choose a level:**
* **Level 1**\
Enables exporting expenses and attachments. Suitable for basic accounting workflows.
* **Level 2 (recommended minimum)**\
Adds Tags Sync, which syncs Accounting Dimensions from the Accounting System into Pleo as Tags. Users assign tags when they submit an expense; bookkeepers can assign or update them further before export.
* **Level 3**\
Adds Chart of Accounts Sync and Tax Code Sync. Chart of Accounts are synced from the Accounting System into Pleo and mapped to expense categories; users assign a category when they submit an expense and bookkeepers can assign or update it before export. Accounts can optionally carry a `taxCodeExternalId` as a reference to a tax code in the Accounting System. Tax codes are synced separately via the Tax Code API.
* **Level 4**\
Adds Vendor Sync, which is bi-directional. Vendors are synced from the Accounting System into Pleo. The integration can also listen for new vendors via the `v1.vendor.created` webhook and update the Accounting System with the details.
## Deployment Overview
Accounting Integrations can be deployed in different ways depending on the target ERP or Accounting System.
At a high level, integrations are deployed either:
* **As a module / on-premises**, hosted inside the ERP environment
* **On a cloud platform**, hosted externally and connected through OAuth 2.0
Each [deployment model](/docs/current/getting-started/accounting-integrations-deployment-decisions) affects how users authenticate, where configuration lives, and how authorisation is handled.
## Authentication & Authorisation
All Accounting Integrations use **OAuth 2.0** to securely access Pleo APIs.
* Users authorise access from within Pleo
* Tokens are issued and managed by the integration
* Scopes determine which APIs and data can be accessed
Authentication behaviour varies slightly depending on deployment model, but **OAuth 2.0 remains the single authorisation mechanism**.
***
## What Comes Next?
Start by getting access to the staging environment and setting up authentication, then build up through the integration levels in order.
**1. Get access**
* [OAuth 2.0 Access to Staging](/docs/current/guides/environment-access/staging-oauth-workflow)
* [OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)
**2. Level 1: Export Integration**
* [Integration Design: Export Overview](/docs/current/integration-design/exports/integration-design-exports-overview)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
**3. Level 2: Tags Sync**
* [Integration Design: Tags Sync Overview](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-overview)
* [Tags Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tags-sync-workflow-guide)
**4. Level 3: Chart of Accounts and Tax Code Sync**
**4.1 Chart of Accounts Sync**
* [Integration Design: Chart of Accounts Sync Overview](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-overview)
* [Chart of Accounts Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/accounts-sync-workflow-guide)
**4.2 Tax Code Sync**
* [Integration Design: Tax Sync Overview](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-overview)
* [Tax Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tax-sync-workflow-guide)
**5. Level 4: Vendor Sync**
* [Vendors API Reference](/reference/vendor/vendors-api-overview)
***
## Related Reading
* [Accounting Integration Deployment Decisions](/docs/current/getting-started/accounting-integrations-deployment-decisions)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# AS/ERP Integration: Quickstart
Source: https://developers.pleo.io/docs/current/getting-started/accounting-integrations-quickstart
This section contains everything you need to build a custom integration between
Pleo and an external Accounting System (AS) or Enterprise Resource Planning (ERP) system.
The documentation is structured to take you from understanding the integration
model through to a complete implementation. It covers four integration levels:
* **Level 1: Expense Items Exporting:** handles the full export workflow from job detection to completion
* **Level 2: Tags Sync:** keeps Accounting Dimensions and their values synchronised from the AS into Pleo as Tags
* **Level 3: Chart of Accounts Sync and Tax Sync:** keeps the Chart of Accounts and Tax Codes synchronised from the Accounting System into Pleo
* **Level 4: Vendors:** keeps Vendors synchronised bi-directionally between Pleo and the Accounting System
## Before You Start
Before building, you need to be onboarded as a Pleo integration partner and
have authentication configured.
Pleo supports two authentication methods:
* **OAuth 2.0:** recommended for most integrations
* **API Keys:** for integrations where OAuth is not supported
See [Connection and Authorisation](/docs/current/integration-design/auth/integration-design-auth-overview)
for setup instructions.
## How This Section Is Organised
The documentation is organised into four layers. Each layer serves a different
purpose, so knowing which to reach for depends on where you are in your work.
### Background
Start here if you are new to Pleo accounting integrations. These pages explain
the integration model, capability levels, and deployment options. Read these
before looking at any implementation detail.
* [AS/ERP Integration Overview](/docs/current/getting-started/accounting-integrations-overview): what the integration covers, integration levels, and key capabilities
* [Deployment Decisions](/docs/current/getting-started/accounting-integrations-deployment-decisions): deployment models, OAuth 2.0 setup, and infrastructure considerations
### Integration Design
These pages explain the rules and requirements for each part of the
integration. They cover what your integration must do, how to handle each
scenario, and what Pleo expects. Read these when you need to understand the
logic behind a specific topic, such as bookkeeping methods or accounts mapping.
Required for all integrations. Covers bookkeeping methods, accounts mapping, data mapping, attachments, and posting behaviour.
Builds on Level 1. Covers Dimension selection, Tag Group and Tag synchronisation rules, data mapping, and sync scheduling.
Covers Chart of Accounts synchronisation rules, reconciliation logic, data mapping, and sync scheduling.
Covers Tax Code synchronisation rules, reconciliation logic, type mapping, and sync scheduling.
Coming soon. In the meantime, refer to the Vendors API Reference.
### Guides
Guides map the end-to-end workflow for each integration level. They are not
implementation references. Their purpose is to show you the complete sequence
of steps, why each step exists, and what the inputs and outputs are at each
stage. Read the relevant guide before writing code so you understand how
everything connects.
#### Level 1: Expense Items Exporting
Two guides work together to cover the full export workflow. Use them as a pair.
The Pleo API side: detecting and starting Export Jobs, retrieving items, updating item statuses, and completing the job.
The AS/ERP side: bookkeeping method resolution, accounts mapping, data mapping, attachments, and posting behaviour.
#### Level 2: Tags Sync
The full Tags Sync cycle: selecting Dimensions, syncing Tag Groups with Dimensions, and syncing Tags with Dimension Values.
#### Level 3: Chart of Accounts Sync and Tax Sync
The full Chart of Accounts Sync cycle: fetching active accounts from the Accounting System, reconciling against Pleo, and applying all required changes.
The full Tax Sync cycle: fetching relevant active tax codes from the Accounting System, reconciling against Pleo, and applying all required changes.
#### Level 4: Vendors
Coming soon. In the meantime, refer to the Vendors API Reference.
### How-to Articles
Each step in the workflow guides links to a corresponding how-to article. The
how-to articles are where the implementation detail lives. Each one covers a
single step and includes:
* step-by-step instructions
* example API requests with curl
* pseudo code explaining the checks and logic
* links to the relevant API reference endpoints
Reach for a how-to article when you are ready to implement a specific step and
need the API calls, request and response examples, and code-level guidance.
Browse the full catalog of how-to articles, organised by integration level.
## Recommended Reading Order
If you are starting from scratch, follow the steps below. Complete each level in order before moving to the next.
#### Level 1: Expense Items Exporting
Read the Overview and Deployment Decisions to understand the integration model and capability levels.
Configure OAuth 2.0 or API Keys before you start building.
Work through the Exports Integration Design pages for the rules behind each topic.
Read the Export Integration and AS/ERP Processing guides as a pair to understand the full sequence.
Use the how-to articles linked from each guide step when you are ready to build.
#### Level 2: Tags Sync
Work through the Tags Sync Integration Design pages for Dimension selection and sync rules.
Read the Tags Sync Workflow Guide to understand the full sync sequence.
Use the how-to articles linked from the guide when you are ready to build.
#### Level 3: Chart of Accounts Sync and Tax Sync
##### Chart of Accounts Sync
Work through the Chart of Accounts Sync Integration Design pages for the rules behind each topic.
Read the Chart of Accounts Sync Workflow Guide to understand the full sync sequence.
Use the how-to articles linked from the guide when you are ready to build.
##### Tax Sync
Work through the Tax Sync Integration Design pages for the reconciliation rules, type mapping, and sync schedule.
Read the Tax Sync Workflow Guide to understand the full sync sequence.
Use the how-to articles linked from the guide when you are ready to build.
#### Level 4: Vendors
Start with the Vendors API Reference to understand the available endpoints and data model.
The full Integration Design, Workflow Guide, and how-to article suite for Vendor Sync is coming soon.
# Early Access Programme (EAP)
Source: https://developers.pleo.io/docs/current/getting-started/developer-partnership-programme
The **Early Access Programme (EAP)** is the entry point for **technology partners** who want to build and distribute integrations with Pleo.
The programme provides a structured path for partners to register their integration intent, align on technical and security expectations, and progress from early testing to a live production integration.
## What is the Early Access Programme (EAP)?
All **partner-built integrations** with Pleo are managed through the Early Access Programme (EAP).
The programme ensures that integrations:
* Meet Pleo’s technical and security standards
* Follow supported authentication and data access models
* Are suitable for use across multiple customer accounts
* Are reviewed before accessing production environments
Access to production APIs is not granted automatically and requires approval through the EAP.
## Who Is This For?
### Pleo Partners
**Pleo Partners** are companies or developer teams building integrations that extend Pleo for multiple customers.
These integrations typically:
* Connect ERP or Accounting Systems to Pleo
* Are designed for reuse across many organisations
* May be distributed through **Pleo’s App Marketplace** or offered as supported partner solutions
This programme is **not intended for individual Pleo customers building internal tools**.
## Business and Technical Requirements
Participation in the EAP is subject to the following conditions:
* **Manual Review:** All partner registrations are reviewed before production access is granted
* **Partner Scope:** Integrations must be designed for multi-customer use
* **Security Standards:** Production access must comply with Pleo’s authentication and data protection requirements
## Authentication Requirements
EAP integrations must use **OAuth 2.0** or **integrated API keys associated with a registered integration**. Standalone API keys are not supported for partner integrations in the Early Access Programme.
Pleo supports the following authentication models for partner integrations:
### OAuth 2.0 (Required Default)
* Required for all partner integrations accessing live customer data
* Supports secure, scalable access across multiple customer accounts
* Enables customers to authorise and revoke access independently
* Provides integration-level identity and auditability
OAuth 2.0 is the preferred and recommended authentication model for all partners.
### Integrated API Keys (Approved Exception Only)
Integrated API keys may be permitted where OAuth 2.0 cannot be supported due to ERP or system constraints.
These keys:
* Are issued as part of a registered integration
* Are tied to a specific partner integration identity
* Allow Pleo to track integration usage and enforce platform controls
* Require explicit approval through the Early Access Programme review process
Standalone API keys created outside of an approved integration are not supported for partner integrations.
Authentication requirement: partner integrations must use OAuth 2.0 or integrated API keys issued for a registered integration. Standalone API keys are not supported under the Early Access Programme.
## What Comes Next?
Once you’ve reviewed the requirements above, you can explore Pleo’s available [environments](/docs/current/getting-started/testing-and-environments) and begin onboarding in **Staging** using **OAuth 2.0**.
During the onboarding process, you’ll be asked to **register your integration intent as part of the Early Access Programme (EAP)**.
This allows Pleo to:
* Understand your integration use case
* Validate technical and security alignment
* Manage progression beyond staging to additional environments
# API Overview
Source: https://developers.pleo.io/docs/current/getting-started/platform-api-overview
This page introduces the **technical foundations** of the Pleo API.
It is intended for **Pleo Partners** building **multi-customer ERP or Accounting System integrations**.
If you are evaluating Pleo for your internal accounting architecture, see the [Platform Overview](/docs/current/getting-started/platform-overview).
## Technical Infrastructure
* **REST API:** Standard HTTP verbs (GET, POST, PUT, DELETE)
* **JSON Format:** Consistent parsing across programming environments
* **OpenAPI Specification:** Full API documentation, client code generation, and automated testing support
## Integration Focus and Marketplace
* **Primary Scope:** ERP and Accounting System integrations built for multiple customers
* **App Marketplace:** Pre-built integrations available for supported systems via the [Pleo App Marketplace](https://www.pleo.io/en/integrations)
## Integration Scenarios
* **Multi-customer Integrations:**\
Build and manage integrations that connect multiple client Accounting Systems to Pleo when pre-built Marketplace solutions are unavailable.
## API Capabilities by Use Case
Pleo’s APIs are designed to support bookkeeping, automation, and integration with downstream systems.
The APIs are organised around **common partner integration scenarios**, rather than individual endpoints.
### Bookkeeping and Accounting Exports
* **Export API**\
Export approved, accounting-ready expense data from Pleo to an external ERP or Accounting System.
### Dimensions and Classification
* **Tags API**\
Assign and manage tags (such as cost centres or dimensions) used for accounting and reporting.
* **Tax Code API**\
Synchronise and manage tax codes used during expense classification and export.
### Automation and Events
* **Webhook Subscriptions API**\
Subscribe to platform events and receive real-time notifications for export jobs and vendor updates.
### User and Identity Data
* **Employees API**\
Retrieve employee details scoped to a company or organisation for reconciliation and automation.
### Marketplace Integrations
* **App Marketplace APIs**\
Enable integrations that connect Pleo with third-party applications distributed through the App Marketplace.
These capabilities can be combined to build end-to-end integrations that synchronise data, automate workflows, and export accounting-ready records at scale.
## What Comes Next?
* **Get started as a Pleo Partner**
Explore the [Early Access Programme (EAP)](/docs/current/getting-started/developer-partnership-programme) to onboard to Staging and register your integration intent.
# Platform Overview
Source: https://developers.pleo.io/docs/current/getting-started/platform-overview
This page explains **how Pleo works as a platform** and where it fits within an ERP or Accounting System (AS) integration.
Use this page to:
* Understand system boundaries and responsibilities
* Evaluate integration feasibility and patterns
* Learn how expense data becomes accounting-ready
If you are new to Pleo, start with: [What is Pleo](/docs/current/getting-started/what-is-pleo).
## System Boundaries
Pleo is a **spend management (pre-accounting) platform**, not an accounting system. It prepares accounting-ready data that is exported or synchronised into your ERP or AS.
| Pleo is responsible for | Your accounting system remains responsible for |
| ------------------------------------------ | ---------------------------------------------- |
| Capturing spend and reimbursements | Ledger integrity |
| Enforcing company policy | Posting rules |
| Collecting required accounting information | Period control |
| Locking data once approved | Financial reporting |
| Exporting immutable accounting records | Statutory compliance |
## Platform Foundations
Pleo provides integrations through four core capabilities that support all integration types:
1. **Master Data Synchronisation:** Ensures employees, legal entities, chart of accounts, vendors, categories, and tags exist in Pleo.
2. **Expense Lifecycle & Approval:** Manages expense entry, validation, and approvals, producing immutable accounting-ready data.
3. **Export Pipeline & Bookkeeping:** Provides a consistent, traceable mechanism to export approved data for downstream systems.
4. **Event-Driven Automation:** Notifies integrations of key events (e.g., export job creation, master data changes) to enable timely reactions.
> These foundations apply regardless of whether you use pre-built or custom integrations.
See [Multi-Entity & API Scope](/docs/current/platform/multi-entity/multi-entity-and-api-scope) for details on company-level API constraints.
## Integration Models
Pleo supports multiple ERP and Accounting System integration models. The same platform capabilities apply regardless of how data is exchanged.
### Pre-Built Integrations
Pleo provides [pre-built](https://www.pleo.io/en/integrations) integrations with selected ERP, Accounting Systems, and HRIS.
* [Configured directly from the Pleo web application](/docs/current/how-tos/how-to-connect-pleo-to-pre-built-as-systems)
* Managed and supported by Pleo
* Designed for standard accounting workflows
* Authentication and authorisation handled internally by the platform
Best suited for organisations using supported systems with standard requirements.
### CSV Export Integrations
For some ERP or accounting workflows, CSV export using ERP-specific templates can be used as an alternative to API integrations.
* Export files are generated from the Pleo Web App
* Data is formatted using ERP-specific CSV templates
* Files can be imported directly into the accounting system
* No API development is required
* Provides a simple and controlled export mechanism
Best suited for organisations that prefer file-based workflows, use systems without API integrations, or require manual review before import.
### Custom (Export-Driven) Integrations
Custom integrations (commonly referred to as Accounting Integrations) use Pleo’s APIs to transfer **accounting-ready data** to external ERP or accounting systems via an **export-driven model**.
In this approach:
* A user initiates **export jobs via the Pleo Web App**
* Immutable, structured JSON records representing approved expenses are produced over the API
* Records are consumed programmatically by the integration or middleware and mapped into the target system
This model gives integrators full control over how and when accounting data is processed, while ensuring consistency, traceability, and auditability.
#### Key Characteristics
* Export jobs are triggered by the user in the Pleo Web App, not directly by the integration
* Immutable, accounting-ready JSON records delivered via API
* Traceable export job and item states
* Clear error reporting and retry mechanisms
* Supports bespoke data mapping and complex accounting logic
* Authenticated using OAuth 2.0 or API Keys
#### Deployment Options
Accounting integrations can be [deployed](/docs/current/getting-started/accounting-integrations-deployment-decisions) in different ways depending on your ERP/Accounting system and integration partner setup:
* **Module / On-Premises**\
Some ERP/accounting systems allow the integration to run within their environment. Benefits include higher stability, tighter connectivity, and leveraging the system’s tech stack for configuration. Typically requires a single **authorisation (OAuth 2.0)** step to access Pleo data.
* **Cloud Platform**\
The integration runs on a cloud platform or middleware. Export jobs are initiated from the Pleo Web App, and your integration consumes the immutable JSON records via the API using **OAuth 2.0**. This approach is common for partner-hosted integrations or more complex multi-system workflows.
#### Event-Driven Notifications (Webhooks)
* Webhooks notify integrations of key events (currently `v1.export.job-created` and `v1.vendor.created`)
* Events act as **triggers**, not data carriers; full data is fetched explicitly via the API
* Integrations may also poll the API instead of relying on webhooks
* Enables near-real-time responsiveness without manual intervention
**Common Webhook Use Cases**
* Reacting to new export jobs
* Monitoring export lifecycle and operational status
* Detecting master data changes (e.g., vendor creation)
Accounting integrations are typically built and operated by partners or customers and are well suited for:
* Unsupported or bespoke ERP / accounting systems
* Complex accounting or multi-entity requirements
* Controlled, batch-based accounting workflows
#### Accounting Integration Lifecycle
A typical Accounting integration follows this lifecycle:
1. Onboard to Staging (OAuth 2.0 or API Keys)
2. Integrate your ERP or Accounting System
3. Synchronise master data into Pleo (e.g., tags, categories, vendors)
4. Capture and approve expenses in Pleo
5. Export accounting-ready data via your integration
6. Monitor jobs and handle exceptions
7. Promote your integration from Staging to Production
#### Platform Guarantees & Constraints
* Only approved and complete expenses are exportable
* Exported expense data is immutable
* Strict legal-entity isolation is enforced
* Master data dependencies must be satisfied
* Rate limits apply per API surface
Specific workflows may vary depending on organisational policies and configuration.\
Pleo adapts to each business while consistently ensuring approved expenses are export-ready.
## What Comes Next?
* **Familiarise yourself with the API surfaces**
Review the [Pleo Platform API Overview](/docs/current/getting-started/platform-api-overview) to see how your integration can interact with master data, the expense lifecycle, and export jobs.
# Environments
Source: https://developers.pleo.io/docs/current/getting-started/testing-and-environments
Pleo provides two environments, **Staging** and **Production**, for **Pleo Partners** building integrations.
All partner integrations are expected to follow a **staging-first workflow** to ensure stability and protect live financial and accounting data.
## Available Environments
### Staging (Partners Only)
Staging is a sandbox environment that allows partners to **build, test, and validate their integrations safely** before using live customer data.
It provides:
* Test partner accounts and test users
* Test financial and expense data
* A safe environment to validate OAuth 2.0 authorisation flows end-to-end
* Full access to staging APIs without impacting production systems
* The ability to test error handling, edge cases, and export workflows
Staging enables partners to complete integration development and confirm correct behaviour before requesting Production access.
Staging access is provided as part of the Early Access Programme (EAP).
### Production
Production is the live environment used for real customer data and financial operations.
It is intended for integrations that:
* Have been fully implemented and tested in Staging
* Have completed required partner review and approval
* Are ready to handle live customer authorisation and data
Production access is granted only after successful validation through the Early Access Programme.
## Moving from Staging to Production
Production access is not available without prior Staging usage.\
All partner integrations must be validated in Staging before they can be promoted.
To request Production access:
* Complete integration testing in Staging
* Confirm that your integration meets Pleo’s technical and security requirements
* Proceed through the required review and approval steps as part of the Early Access Programme (EAP)
Once approved, Production credentials and environment details will be provided.
## OAuth 2.0 Implementation and Testing Workflow
To implement and validate OAuth 2.0 authentication, partners typically follow these steps:
1. **Obtain an OAuth 2.0 App Client**
Request an [OAuth 2.0 client (App Client)](/docs/current/how-tos/oauth/how-to-register-an-oauth-client), which provides:
* Client ID
* Client Secret
This allows your integration to initiate the OAuth 2.0 authorisation flow.
2. **Implement the OAuth 2.0 authorisation flow**
Use the Client ID and Client Secret to:
* [Redirect users to Pleo’s authorisation endpoint](/docs/current/guides/oauth-workflow-guide#2-direct-users-to-authorisation-endpoint)
* [Exchange the authorisation code for access tokens](/docs/current/guides/oauth-workflow-guide#3-handle-redirect-&-exchange-authorisation-code)
* [Use the access token to call Pleo APIs](/docs/current/guides/oauth-workflow-guide#4-use-access-token-to-call-apis)
* [Store and use access tokens securely](/docs/current/guides/oauth-workflow-guide#5-refresh-tokens-centrally)
3. **Use Staging accounts to test the flow**
Staging accounts provide:
* Test users
* Test financial data
* A safe environment to authorise your integration and obtain access tokens
4. **Validate API access using OAuth 2.0 access tokens**
Use the issued access token to [make authenticated API calls](/docs/current/how-tos/oauth/how-to-call-pleo-apis-using-access-tokens) and confirm correct integration behaviour.
Common OAuth 2.0 pitfall: the Client Secret is never used as an API credential.
API requests must use an OAuth 2.0 access token in the Authorization header:
```html theme={null}
Authorization: Bearer # OAuth 2.0 access token
```
The Client Secret is only used during the OAuth 2.0 flow to obtain access tokens.
## What Comes Next?
Next steps depend on how your integration authenticates with Pleo:
* **Pleo Partners (OAuth 2.0):**\
Follow the [OAuth 2.0 Access to Staging](/docs/current/guides/environment-access/staging-oauth-workflow) guide to configure and test authentication in Staging.
## Related Reading
* [Accounting Integration Overview](/docs/current/getting-started/accounting-integrations-overview): A high-level overview of Accounting Integrations between Pleo and external ERP or Accounting Systems.
# What is Pleo
Source: https://developers.pleo.io/docs/current/getting-started/what-is-pleo
Pleo is a **spend management platform** that sits between company spending and downstream **ERP** and **Accounting Systems (AS)**.
It captures, validates, and prepares expense data so that only **complete, compliant, and accounting-ready records** reach your general ledger.
Jump to Quickstart Guide
## Who Pleo Is For
Pleo is used by employees who spend company money (**spenders**) and by finance teams who review, control, and export that spend.
* **Employees (spenders)**\
Submit expenses, attach receipts, and provide required information at the point of spend.
* **Finance teams**\
Review spend, enforce company policy, and ensure expense data is complete before export to accounting systems.
* **Developers and architects**\
Integrate clean, structured expense data into ERP and Accounting Systems via well-defined APIs.
## Where Pleo Fits
Pleo operates **upstream of accounting systems**.
* Employees spend money using cards or submit expenses.
* Pleo helps finance teams review, validate, and approve expenses through workflows, controls, and supporting information.
* Only approved data is exported to ERP or Accounting Systems.
Pleo does **not** replace your general ledger, reporting, or tax engines. It prepares expense data *before* bookkeeping.
## Why Integrate with Pleo?
* Spend validation happens upstream.
* Accounting systems receive immutable, ready-to-book data.
* Clear system boundaries reduce integration complexity.
## What Comes Next?
* **Understand the platform’s role in your workflow**
Explore [Platform Overview](/docs/current/getting-started/platform-overview) to see how Pleo captures, validates, and exports expense data for your ERP or Accounting System.
# Chart of Accounts Sync Workflow Guide
Source: https://developers.pleo.io/docs/current/guides/accounting-integrations/imports/accounts-sync-workflow-guide
This guide covers [Integration Level 3](/docs/current/getting-started/accounting-integrations-overview), which enables syncing the Chart of Accounts from your Accounting System into Pleo. It builds on Level 2 (Tags Sync) and ensures bookkeepers can assign the correct accounts to expense categories before export.
## What You'll Have Built
After implementing this workflow:
* Active accounts from the AS are reflected as Accounts in Pleo.
* Accounts are automatically kept up to date on a scheduled basis.
* Bookkeepers can assign current, accurate accounts to expense categories in Pleo.
* The integration aligns with Pleo's Chart of Accounts Sync guarantees and matching model.
## Who This Guide Is For
This guide is intended for:
* Integration developers
* Solution architects
* Accounting platform integrators
It focuses on **workflow understanding**, not implementation details.
## Before You Start
You should be familiar with:
* Pleo's [supported authentication](/docs/current/integration-design/auth/integration-design-auth-overview#authentication-policy-overview) methods.
* The [Chart of Accounts Sync](/docs/current/platform/accounting-integrations/imports/accounts/accounts-sync-overview) platform capabilities page.
## Chart of Accounts Sync Workflow Overview
The Chart of Accounts in the Accounting System is the definitive list of accounts used to classify and bookkeep expenses. Each account has a code (the account number visible to admins/bookkeepers), a name, and a stable `externalId` that uniquely identifies it in the AS.
Chart of Accounts Sync copies this structure into Pleo. The Accounting System is the source of truth; the integration's role is to keep Pleo's Accounts aligned with the current state of the AS so bookkeepers always have accurate, up-to-date options when assigning accounts to expense categories.
Unlike Tags Sync, Chart of Accounts Sync does not have a selection step. All active accounts from the AS are synchronised into Pleo.
The how-to articles in this section use a consistent example to illustrate each step. The table below shows the starting state in both systems before the sync runs.
| Account (AS) | AS Status | Account (Pleo) | Pleo Status | Desired Pleo State |
| ---------------------- | --------- | ---------------------- | -------------- | -------------------- |
| 1000 - Office Supplies | Active | 1000 - Office Supplies | Active | Active (no change) |
| 2000 - Travel | Active | 2000 - Travel | Archived | Unarchived |
| 3000 - Software | Active | — | Does not exist | Created |
| 4000 - Marketing | Active | 4000 - Advertising | Active | Updated to match AS |
| — | — | 5000 - Entertainment | Active | Archived (not in AS) |
The two steps progress as follows:
| Step | Integration action | Outcome |
| --------------------------------------- | ----------------------------------------------------------- | --------------------------------------------- |
| 1. Fetch and Match Accounts | Fetches accounts from AS and Pleo, matches by `externalId` | Each account is paired with a required action |
| 2. Create, Update, and Archive Accounts | Creates, updates, unarchives, and archives Accounts in Pleo | Pleo reflects the current Chart of Accounts |
***
## Steps
### 1. Fetch and Match Accounts
#### Purpose
This step runs at the start of every Chart of Accounts Sync cycle. The integration retrieves all active accounts from the Accounting System and all Accounts from Pleo (active and archived), then matches them using the `externalId` field to determine what action is needed for each account.
#### Input
* Active accounts from the AS Chart of Accounts
* Active and archived Accounts from Pleo. Archived Accounts are included so they can be unarchived rather than duplicated if an account becomes active in the AS again.
#### Workflow Process
#### Output
* Each AS account matched to a Pleo Account (or identified as new)
* Each Pleo Account identified as requiring creation, unarchiving, updating, or no action
* Active Pleo Accounts with no AS match flagged for archiving
#### Why It Matters
The matching step is the foundation of the reconciliation. Accurate matching by `externalId` ensures accounts are updated rather than duplicated, and that accounts removed from the AS are archived rather than left stale in Pleo.
#### Integration Design
If you're an integration developer or architect, read the [Sync Accounts](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-sync) integration design doc before implementing this step. It covers the matching rules, archiving behaviour, the `taxCodeExternalId` reference field, and how the sync handles each reconciliation outcome.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying how-to article.
How to Fetch and Match Accounts
***
### 2. Create, Update, and Archive Accounts
#### Purpose
This step runs immediately after the matching step. For each account requiring action, the integration creates, updates, unarchives, or archives the corresponding Account in Pleo. After all AS accounts are processed, any active Pleo Account with no matching AS account is archived.
#### Input
* Active accounts from the AS Chart of Accounts (used to drive create, update, and unarchive operations)
* The matched account set from Step 1, identifying which Pleo Accounts require action and which active Pleo Accounts have no AS match
#### Workflow Process
#### Output
* Accounts created for AS accounts with no existing match in Pleo
* Archived Accounts unarchived where an active AS account matches
* Account names and codes updated where they differ from the current AS
* Active Accounts archived where their AS account is no longer active
#### Why It Matters
These write operations bring Pleo's Chart of Accounts into alignment with the AS. Without them, the account list in Pleo will drift out of date, causing bookkeepers to assign incorrect accounts to expense categories before export.
#### Integration Design
If you're an integration developer or architect, read the [Sync Accounts](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-sync) integration design doc before implementing this step. It covers the API operations, request and response structure, and the data mapping requirements.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying how-to article.
How to Create, Update, and Archive Accounts
***
## What Comes Next?
Once Chart of Accounts Sync is live, admin/bookkeepers and spenders can start using the synced accounts in Pleo:
* [How to set up your categories](https://help.pleo.io/en/support/solutions/articles/103000254857-how-to-set-up-your-categories): See how Admins/Bookkeepers map accounts to expense categories in Pleo.
* [Get reimbursed for out-of-pocket expenses](https://help.pleo.io/en/support/solutions/articles/103000281947-get-reimbursed-for-out-of-pocket-expenses): See how spenders assign a category when submitting an expense via the Pleo Mobile App.
To extend your integration further:
* [Tax Sync](/reference/tax-codes-overview): Sync tax codes from your Accounting System into Pleo.
***
## Related Reading
* [Integration Design: Chart of Accounts Sync Overview](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-overview)
* [Platform Capabilities: Chart of Accounts Sync](/docs/current/platform/accounting-integrations/imports/accounts/accounts-sync-overview)
***
# Tags Sync Workflow Guide
Source: https://developers.pleo.io/docs/current/guides/accounting-integrations/imports/tags-sync-workflow-guide
This guide covers [Integration Level 2](/docs/current/getting-started/accounting-integrations-overview), which enables syncing Accounting Dimensions from your Accounting System into Pleo as Tags. It builds on Level 1 (Expense Items Exporting) and ensures bookkeepers can assign the correct Dimensions to expenses before export.
## What You'll Have Built
After implementing this workflow:
* Accounting Dimensions from the AS are reflected as Tag Groups in Pleo.
* Dimension Values are reflected as Tags within each Tag Group.
* Tags are automatically kept up-to-date on a scheduled basis.
* Bookkeepers can assign current, accurate Dimensions to expenses in Pleo.
* The integration aligns with Pleo's Tags Sync guarantees and matching model.
## Who This Guide Is For
This guide is intended for:
* Integration developers
* Solution architects
* Accounting platform integrators
It focuses on **workflow understanding**, not implementation details.
## Before You Start
You should be familiar with:
* Pleo's [supported authentication](/docs/current/integration-design/auth/integration-design-auth-overview#authentication-policy-overview) methods.
* The [Tags Sync](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview) platform capabilities page.
## Tags Sync Workflow Overview
In the Accounting System, Dimensions represent categories used to classify expenses (such as Department, Cost Centre, or Project). Each Dimension contains Dimension Values, which are the individual options a bookkeeper can assign to an expense (such as Engineering, Marketing, or Operations).
Tags Sync maps this structure into Pleo. Dimensions become Tag Groups and Dimension Values become Tags. The Accounting System is the source of truth; the integration's role is to keep Pleo's Tag Groups and Tags aligned with the current state of the AS so bookkeepers always have accurate, up-to-date options when coding expenses.
Step 1 runs once on first connection and configures which Dimensions to synchronise. Steps 2 and 3 run together on a schedule, keeping Pleo in sync with the AS as Dimensions and Dimension Values change over time. Each step produces outputs required by the next.
The how-to articles in this section use a consistent example to illustrate each step. The table below shows the starting state in both systems before the sync process runs.
| Dimension (AS) | AS Status | Tag Group (Pleo) | Pleo Status | Desired Pleo State |
| -------------- | --------- | ---------------- | -------------- | ------------------------------ |
| Department | Active | Department | Active | Active (no change) |
| Project | Active | Project | Active | Active (no change) |
| Finance | Active | — | Does not exist | Created |
| Cost Centre | Active | Cost Centre | Archived | Unarchived |
| Supplier | Active | — | Does not exist | Created |
| — | — | Region | Active | Archived (doesn't exist in AS) |
The three steps progress as follows:
| Step | Integration action | Outcome |
| -------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------ |
| 1. Select Dimensions | Selects Department, Project, Finance, Cost Centre, and Supplier | 5 Dimensions stored in the integration's configuration |
| 2. Sync Tag Groups | Creates Finance and Supplier Tag Groups, unarchives Cost Centre, archives Region | Tag Groups in Pleo aligned with selected Dimensions |
| 3. Sync Tags | Creates, updates, and archives Tags to match Dimension Values from the AS | Tags in Pleo aligned with current AS Dimension Values |
***
## Steps
### 1. Select Dimensions
#### Purpose
Before Tags Sync can run, the integration must know which Accounting Dimensions from the AS to synchronise. This step configures that selection and stores it for every subsequent sync cycle.
On **first connection**, the integration automatically pre-selects Dimensions whose code matches an existing active Tag Group in Pleo. This reduces setup effort for integrators who have previously configured Tags manually in Pleo. Any remaining Dimensions can be selected manually, up to a maximum of 5 in total.
On **subsequent connections**, the stored selection is restored from the integration's configuration. No re-selection is needed. If a previously selected Dimension has become inactive in the AS since the last sync, it is automatically deselected and its corresponding Tag Group is archived in Pleo.
#### Input
* Active Dimensions from the AS (only active Dimensions are eligible for selection)
* Existing active Tag Groups from Pleo, used to determine which Dimensions can be auto-selected on first connection. If there are no active Tag Groups in Pleo, nothing is auto-selected and all Dimensions must be selected manually.
#### Workflow Process
#### Output
* Dimensions auto-selected where their code matches an active Pleo Tag Group (first connection only)
* Dimensions that have become inactive in the AS automatically removed from the selection
* Up to 5 Dimensions stored in the integration's configuration, ready for use in every sync cycle
#### Why It Matters
The Dimension selection defines the scope of every sync cycle that follows. Only selected, active Dimensions are synchronised, so the selection must reflect what bookkeepers actually need to code expenses against. Auto-selection eliminates manual setup when reconnecting or reconfiguring after Tags have already been created in Pleo, and automatic deselection of inactive Dimensions keeps the selection clean without requiring manual intervention.
#### Integration Design
If you're an integration developer or architect, read the [Dimension Selection](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-dimension-selection) integration design doc before implementing this step. It covers the rules and constraints your implementation must comply with, including selection limits, auto-selection matching logic, how inactive Dimensions are handled, and what deselecting a Dimension does to the corresponding Tag Group in Pleo.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying How-to article.
How to Select Dimensions for Tags Sync
***
### 2. Sync Tag Groups with Dimensions
#### Purpose
This step runs at the start of every Tags Sync cycle, immediately after the Dimension selection is confirmed. The integration retrieves the selected Dimensions from its configuration, then retrieves all Tag Groups from Pleo, both active and archived, and aligns them.
For each selected Dimension, the integration looks for a matching Tag Group in Pleo by code (case-insensitive). If a match is found and the Tag Group is archived, it is unarchived. If no match exists, a new Tag Group is created. If the Tag Group name differs from the AS, it is updated to match.
Any active Tag Group in Pleo that no longer has a matching selected Dimension is archived. This covers Dimensions that were deselected or became inactive since the last sync cycle.
#### Input
* Selected active Dimensions from the integration's configuration (the Dimensions chosen in Step 1 that are still active in the AS)
* Active and archived Tag Groups from Pleo. Archived Tag Groups are included so they can be unarchived rather than duplicated if a Dimension is re-selected.
#### Workflow Process
#### Output
* Tag Groups created for Dimensions with no existing match in Pleo
* Archived Tag Groups unarchived where a selected Dimension matches
* Tag Group names updated where they differ from the current AS Dimension name
* Active Tag Groups archived where their Dimension is no longer selected
#### Why It Matters
Tag Groups are the containers that hold Tags. If this step is not run correctly, the structure bookkeepers rely on to code expenses will be out of date. Tag Group sync ensures the Pleo structure mirrors the AS before any Tags are processed, so Tags always end up in the right place.
#### Integration Design
If you're an integration developer or architect, read the [Sync Tag Groups with Dimensions](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tag-groups) integration design doc before implementing this step. It covers the matching rules, archiving behaviour, and how the sync handles inactive Dimensions discovered during the cycle.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying How-to article.
How to Sync Tag Groups with Accounting Dimensions
***
### 3. Sync Tags with Dimension Values
#### Purpose
This step runs immediately after Tag Group sync, once for every active Tag Group in Pleo. For each Tag Group, the integration retrieves the corresponding Dimension Values from the AS and the current Tags from Pleo, then aligns them.
Tags are matched to Dimension Values by code (case-insensitive). Matched Tags that are archived are unarchived. Tags whose names differ from the AS are updated. If a Dimension Value has no matching Tag, a new Tag is created. Active Tags with no matching Dimension Value are archived.
The step also handles duplicate Tags (where multiple Tags in Pleo share the same code) by retaining the best match and archiving the rest.
#### Input
* Active Dimension Values from the AS for each selected Dimension (only active values are processed; inactive, blocked, or expired values result in the corresponding Tag being archived)
* Active and archived Tags from Pleo for each Tag Group. Archived Tags are included so they can be unarchived if the corresponding Dimension Value becomes active again.
#### Workflow Process
#### Output
* Tags created for Dimension Values with no existing match in Pleo
* Archived Tags unarchived where a Dimension Value matches
* Tag names updated where they differ from the current AS Dimension Value name
* Active Tags archived where their Dimension Value is no longer active in the AS
* Duplicate Tags resolved by retaining the best match and archiving the rest
#### Why It Matters
Tags are what bookkeepers see and use when coding expenses. If Tags are out of date, bookkeepers may assign incorrect Dimensions, leading to errors when expenses are exported to the AS. This step ensures every active Dimension Value in the AS is available as a Tag in Pleo, and that stale Tags are removed before they cause problems.
#### Integration Design
If you're an integration developer or architect, read the [Sync Tags with Dimension Values](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tags) integration design doc before implementing this step. It covers matching rules, duplicate resolution logic, the conditions under which Tags are archived, and how the sync handles edge cases like expired or blocked Dimension Values.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying How-to article.
How to Sync Tags with Dimension Values
***
## What Comes Next?
After implementing Tags Sync, you can extend your integration with:
* [Chart of Accounts Sync](/reference/accounts/chart-of-accounts-api-overview)
* [Tax Sync](/reference/tax-codes-overview)
***
## Related Reading
* [Integration Design - Tags Sync Overview](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-overview)
* [Platform Capabilities — Tags Sync](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview)
***
# Tax Sync Workflow Guide
Source: https://developers.pleo.io/docs/current/guides/accounting-integrations/imports/tax-sync-workflow-guide
This guide covers [Integration Level 3](/docs/current/getting-started/accounting-integrations-overview), which enables syncing Tax Codes from your Accounting System (AS) into Pleo. It builds on Level 3 (Chart of Accounts Sync) and ensures bookkeepers can assign the correct tax codes to expenses before export.
## What You'll Have Built
After implementing this workflow:
* Active, relevant tax codes from the AS are reflected as Tax Codes in Pleo.
* Tax Codes are automatically kept up to date on a scheduled basis.
* Bookkeepers can assign current, accurate tax codes to expenses in Pleo.
* The integration aligns with Pleo's Tax Sync guarantees and matching model.
## Who This Guide Is For
This guide is intended for:
* Integration developers
* Solution architects
* Accounting platform integrators
It focuses on **workflow understanding**, not implementation details.
## Before You Start
You should be familiar with:
* Pleo's [supported authentication](/docs/current/integration-design/auth/integration-design-auth-overview#authentication-policy-overview) methods.
* The [Tax Sync](/docs/current/platform/accounting-integrations/imports/tax-codes/tax-sync-overview) platform capabilities page.
## Tax Sync Workflow Overview
Tax codes in the AS are the definitive list of tax rates used to classify expenses for reporting and compliance. Each tax code has a code (the identifier visible across systems), a name, a rate, and a type.
Tax Sync copies this structure into Pleo. The Accounting System is the source of truth; the integration's role is to keep Pleo's Tax Codes aligned with the current state of the AS so bookkeepers always have accurate, up-to-date options when assigning tax codes to expenses.
Unlike Tags Sync, Tax Sync does not have a user-driven selection step. All relevant active tax codes from the AS are synchronised into Pleo after filtering out irrelevant types (such as Sales VAT) and inactive codes.
The how-to articles in this section use a consistent example to illustrate each step. The table below shows the starting state in both systems before the sync runs.
| Tax Code (AS) | AS Status | Tax Code (Pleo) | Pleo Status | Desired Pleo State |
| ---------------------- | --------- | ---------------------- | -------------- | -------------------- |
| VAT20 - Standard (20%) | Active | VAT20 - Standard (20%) | Active | Active (no change) |
| VAT5 - Reduced (5%) | Active | VAT5 - Reduced (5%) | Archived | Unarchived |
| VAT0 - Zero Rated (0%) | Active | — | Does not exist | Created |
| REV - Reverse Charge | Active | REV - Reverse Charge | Active | Updated to match AS |
| — | — | EXEMPT - Exempt | Active | Archived (not in AS) |
The two steps progress as follows:
| Step | Integration action | Outcome |
| ---------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------- |
| 1. Fetch and Match Tax Codes | Fetches tax codes from AS and Pleo, matches by `code` | Each tax code is paired with a required action |
| 2. Create, Update, and Archive Tax Codes | Creates, updates, unarchives, and archives Tax Codes in Pleo | Pleo reflects the current active, relevant tax codes |
***
## Steps
### 1. Fetch and Match Tax Codes
#### Purpose
This step runs at the start of every Tax Sync cycle. The integration retrieves relevant active tax codes from the AS and all Tax Codes from Pleo (active and archived), then matches them using the `code` field to determine what action is needed for each tax code.
#### Input
* Relevant active tax codes from the AS (filtered to exclude Sales VAT, blocked, or inactive codes)
* Active and archived Tax Codes from Pleo. Archived Tax Codes are included so they can be unarchived rather than duplicated if a tax code becomes active in the AS again.
#### Workflow Process
The integration retrieves and filters AS tax codes, then retrieves all Pleo Tax Codes. It matches every AS tax code against Pleo Tax Codes by `code` (case insensitive) and determines the required action for each.
#### Output
* Each AS tax code matched to a Pleo Tax Code (or identified as new)
* Each Pleo Tax Code identified as requiring creation, unarchiving, updating, or no action
* Active Pleo Tax Codes with no AS match flagged for archiving
#### Why It Matters
The matching step is the foundation of the reconciliation. Accurate matching by `code` ensures tax codes are updated rather than duplicated, and that tax codes removed from the AS are archived rather than left stale in Pleo.
#### Integration Design
If you're an integration developer or architect, read the [Sync Tax Codes](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync) integration design doc before implementing this step. It covers the matching rules, type mapping, duplicate handling behaviour, and how the sync handles each reconciliation outcome.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying how-to article.
How to Fetch and Match Tax Codes
***
### 2. Create, Update, and Archive Tax Codes
#### Purpose
This step runs immediately after the matching step. For each tax code requiring action, the integration creates, updates, unarchives, or archives the corresponding Tax Code in Pleo. After all AS tax codes are processed, any active Pleo Tax Code with no matching AS tax code is archived.
#### Input
* Relevant active tax codes from the AS (used to drive create, update, and unarchive operations)
* The matched tax code set from Step 1, identifying which Pleo Tax Codes require action and which active Pleo Tax Codes have no AS match
#### Workflow Process
For each tax code identified in Step 1:
* **Create** new Tax Codes for AS tax codes with no existing match in Pleo
* **Unarchive** Tax Codes where an active AS tax code matches an archived Pleo Tax Code
* **Update** Tax Code names, rates, and types where they differ from the current AS
* **Archive** active Tax Codes where their AS tax code is no longer active
* **Deduplicate** if two Pleo Tax Codes share the same code: retain the one matching the AS rate, archive the other
#### Output
* Tax Codes created for AS tax codes with no existing match in Pleo
* Archived Tax Codes unarchived where an active AS tax code matches
* Tax Code names, rates, and types updated where they differ from the current AS
* Active Tax Codes archived where their AS tax code is no longer active
#### Why It Matters
These write operations bring Pleo's Tax Codes into alignment with the AS. Without them, the tax code list in Pleo will drift out of date, causing bookkeepers to assign incorrect tax codes to expenses before export.
#### Integration Design
If you're an integration developer or architect, read the [Sync Tax Codes](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync) integration design doc before implementing this step. It covers the API operations, request and response structure, and the data mapping requirements.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying how-to article.
How to Create, Update, and Archive Tax Codes
***
## What Comes Next?
Once Tax Sync is live, bookkeepers can assign the synced tax codes to expenses in Pleo before export.
To extend your integration further:
* [Vendor Sync](/docs/current/getting-started/accounting-integrations-overview): Sync vendors between Pleo and your Accounting System (Level 4).
***
## Related Reading
* [Integration Design: Tax Sync Overview](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-overview)
* [Platform Capabilities: Tax Sync](/docs/current/platform/accounting-integrations/imports/tax-codes/tax-sync-overview)
***
# Workflow Guide Catalogue
Source: https://developers.pleo.io/docs/current/guides/accounting-integrations/workflow-guides-overview
End-to-end workflow guides for building accounting integrations with Pleo. Read the relevant guide before implementing so you understand the full sequence of steps and how everything connects.
## Exports
The Pleo API side: detecting and starting Export Jobs, retrieving items, updating item statuses, and completing the job.
The AS/ERP side: bookkeeping method resolution, accounts mapping, data mapping, attachments, and posting behaviour.
## Imports
The full Tags Sync cycle: configuring Dimension selection, syncing Tag Groups with Dimensions, and syncing Tags with Dimension Values.
The full Chart of Accounts Sync cycle: fetching and matching accounts, then creating, updating, unarchiving, and archiving accounts in Pleo.
The full Tax Sync cycle: fetching and matching tax codes, then creating, updating, unarchiving, and archiving Tax Codes in Pleo.
# AS/ERP Processing Workflow Guide
Source: https://developers.pleo.io/docs/current/guides/accounting-system-processing-workflow-guide
This guide covers the AS/ERP processing phase of [Integration Level 1](/docs/current/getting-started/accounting-integrations-overview). It explains how Export Items fetched from Pleo are transformed into accounting entries inside the Accounting System or ERP.
## What You'll Have Built
After implementing this workflow:
* Each Export Item correctly routed to a journal entry or accounts payable workflow based on its bookkeeping method.
* GL accounts resolved and balanced for every accounting entry.
* Expense data accurately mapped into the Accounting System's required fields, including amounts, dates, tax codes, and dimensions.
* Receipts and supporting documents attached to their corresponding accounting entries.
* Entries assigned to the correct accounting periods, with closed period scenarios handled.
* Entries created with the correct posting status (draft or finalised) per integration configuration.
* Predictable, auditable accounting outcomes for every Export Item in the batch.
## Who This Guide Is For
This guide is intended for:
* Integration developers implementing exports
* Solution architects designing accounting integrations
* Technical partners building Accounting System connectors
## Before You Start
You should be familiar with:
* The [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide), which covers how Export Jobs are detected and Export Items are fetched from Pleo.
* This guide begins where that guide's [Step 4 (Fetch Export Item Data)](/docs/current/guides/export-integration-workflow-guide#4-fetch-export-item-data-data-layer) ends. Export Items must already be fetched and validated before this workflow starts.
## AS/ERP Processing Workflow Overview
Once Export Items have been fetched from Pleo, the integration processes each Export Item into an accounting entry inside the Accounting System. The six steps in this guide cover everything from determining how the expense should be recorded, through accounts and data mapping, attachment handling, and period assignment, to creating the entry with the correct posting status.
Each step is applied per Export Item. A failure at any step for a given Export Item is recorded as a failed Export Item and does not block other Export Items from being processed. Once all Export Items are processed, the integration returns to the Export Integration Workflow to report outcomes back to Pleo and complete the Export Job.
***
## Steps
### 1. Determine Bookkeeping Method
#### Purpose
Before any accounting entry can be created, the integration must determine how each expense should be recorded. Pleo provides the bookkeeping method for each Export Item via the `bookkeeping.method` field. The integration reads this value and routes processing accordingly: `journal` produces a standard journal entry, `accounts_payable` produces a vendor-based invoice and payment, and `none` is treated as `journal`. Pleo sets `none` for non-payable balance adjustments such as wallet loads, wallet unloads, and balance amendments.
#### Input
The `bookkeeping.method` field from the Export Item, with one of four values: `journal`, `accounts_payable`, `none` (non-payable balance adjustments such as wallet loads and balance amendments, always treated as journal entry), or `null` (legacy support).
#### Workflow Process
#### Output
* Bookkeeping method resolved for this Export Item: journal entry or accounts payable
* Integration routed to the appropriate recording workflow for the Export Item
#### Why It Matters
The bookkeeping method determines the entire structure of the accounting entry. Journal entries and accounts payable records have different debit/credit structures, different fields, and different downstream behaviour in the Accounting System. Resolving the method first ensures every subsequent mapping and posting step operates on the correct structure for the Export Item.
#### Integration Design
If you're an integration developer or architect, read the [Bookkeeping Method Resolution](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-resolution) integration design doc, as well as [Journal Entry Handling](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-journal-entry) and [Accounts Payable Handling](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-accounts-payable), before implementing this step. They cover the resolution logic for each possible value of `bookkeeping.method`, including how `none` is handled, and the specific entry structures required for journal entries and accounts payable.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying How-to article.
How to Determine the Bookkeeping Method
***
### 2. Apply Accounts Mapping
#### Purpose
With the bookkeeping method known, the integration resolves which GL accounts to use for the debit and credit sides of the entry. This includes the expense account (determined by the expense category), the contra account (wallet, bank, or clearing), and for accounts payable entries, the vendor account. The integration relies on its own mapping configuration and the Accounting System to validate that all resolved accounts exist and are active.
#### Input
* The expense category from the Export Item, used to determine the expense GL account
* Wallet, contra, and out-of-pocket account configuration from the integration
* Vendor configuration from the integration and the Accounting System (for accounts payable entries)
* The bookkeeping method resolved in Step 1
#### Workflow Process
#### Output
* All debit and credit GL accounts resolved for this entry
* Accounts validated as existing and active in the Accounting System
* The integration knows exactly which accounts to debit and credit to produce a balanced accounting record
#### Why It Matters
Incorrect accounts mapping produces entries that post to the wrong GL accounts, distorting financial statements, balance sheets, and reconciliations. Getting mapping right is fundamental to the accuracy of the Accounting System and directly affects whether period-end reporting is reliable.
#### Integration Design
If you're an integration developer or architect, read the [Accounts Mapping](/docs/current/integration-design/exports/integration-design-exports-accounts-mapping) integration design doc before implementing this step. It covers the mapping rules for each account type, how the bookkeeping method affects which accounts are required, expense type rules, and how to handle missing or invalid accounts.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying How-to article.
How to Apply Accounts Mapping
***
### 3. Apply Data Mapping
#### Purpose
With accounts resolved, the integration maps the financial data from the Export Item into the fields required by the Accounting System. This covers mandatory fields that every entry must include (amounts, dates, currency, and unique identifiers), invoice-specific fields for accounts payable entries (vendor references and payment terms), optional fields where available (dimensions, tags, and notes), and VAT handling rules where the Accounting System requires tax information.
#### Input
* The full Export Item payload from [Step 4 of the Export workflow](/docs/current/guides/export-integration-workflow-guide#4-fetch-export-item-data-data-layer), containing all financial data provided by Pleo
* Integration configuration for dimensions, tax codes, and optional field mappings
#### Workflow Process
#### Output
* A complete, accounting-ready payload with all required fields mapped to the Accounting System's expected structure
* VAT and tax information applied where required
* The entry is ready to be assigned a period and posted
#### Why It Matters
Accurate data mapping ensures every accounting entry contains the correct amounts, references, tax treatment, and dimensions. Incomplete or incorrect mapping leads to reporting errors, failed reconciliations, and compliance issues that are difficult and costly to correct after posting.
#### Integration Design
If you're an integration developer or architect, read the [Data Mapping](/docs/current/integration-design/exports/integration-design-exports-data-mapping) integration design doc before implementing this step. It covers mandatory, invoice-specific, and optional field mappings, VAT handling rules, and how to handle missing or unsupported fields.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying How-to article.
How to Apply Data Mapping
***
### 4. Transfer Attachments
#### Purpose
Receipts and supporting documents associated with each Export Item must be downloaded from Pleo and uploaded to the Accounting System, where they are linked to the accounting entry. The integration must handle cases where no attachments exist (skipping cleanly), where multiple attachments are present, and where upload failures occur. Idempotency is important: re-running this step should not produce duplicate attachments.
#### Input
* Attachment references from the Export Item payload, including download URLs and metadata for each receipt or supporting document
* The accounting entry reference, used to link uploaded attachments to the correct record in the Accounting System
#### Workflow Process
#### Output
* Receipts and supporting documents uploaded to the Accounting System and linked to the corresponding accounting entry
* Items with no attachments skipped without error
* Upload failures recorded and handled without blocking other Export Items in the batch
#### Why It Matters
Attached receipts are essential for audit trails, expense compliance, and bookkeeper review. Without them, accountants must retrieve receipts manually from Pleo or original sources, increasing workload and the risk of documentation gaps during audits or compliance checks.
#### Integration Design
If you're an integration developer or architect, read the [Attachment Handling](/docs/current/integration-design/exports/integration-design-exports-attachment-handling) integration design doc before implementing this step. It covers the attachment retrieval process, how to handle multiple attachments, failure handling, and idempotency requirements.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying How-to article.
How to Transfer Attachments
***
### 5. Assign Accounting Period
#### Purpose
The integration must determine which accounting period the entry belongs to and post it to that period. The default posting date is the expense transaction date from the Export Item. If the Accounting System rejects the posting because the period is closed, the integration applies a configured closed-period strategy: either adjusting the posting date to the next open period and retrying, or failing the Export Item with a clear reason. Once posting succeeds, the integration confirms which period the entry was assigned to using the final posting date.
#### Input
* The expense date (`exportItem.date`) as the default posting date
* Integration configuration for the closed-period handling strategy: adjust and retry, or fail the Export Item
* Accounting System period rules, fiscal calendar, and open period information
#### Workflow Process
#### Output
* The accounting entry assigned to a valid open period using the correct posting date
* Entries rejected due to closed periods either reassigned to an open period (if adjustment is enabled) or failed with a clear reason
* The final resolved accounting period confirmed
#### Why It Matters
Period assignment determines where entries appear in financial reports. An entry posted to the wrong period distorts profit and loss statements and makes period-end reconciliation unreliable. Handling closed periods correctly prevents entries from being silently rejected or posted to incorrect periods without the bookkeeper being aware.
#### Integration Design
If you're an integration developer or architect, read the [Accounting Periods](/docs/current/integration-design/exports/integration-design-exports-accounting-periods) integration design doc before implementing this step. It covers the expense date vs posting date distinction, the closed-period handling strategies, how to resolve accounting periods for custom or fiscal calendars, and the service period field.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying How-to article.
How to Assign Accounting Periods
***
### 6. Apply Posting Behaviour
#### Purpose
Before creating the accounting entry, the integration applies the configured posting behaviour: whether entries should be created as drafts (held for bookkeeper review before affecting financial statements) or finalised (posted immediately). The Accounting System may use different terminology for these states, and the integration must translate its configuration into the closest equivalent state supported by the system. If no posting configuration is found, the integration defaults to draft posting.
#### Input
* Integration posting configuration: draft status enabled or disabled
* The Accounting System's supported posting states and their labels (e.g. draft/posted, approved/unapproved, saved/committed)
#### Workflow Process
#### Output
* The accounting entry created in the Accounting System with the correct posting status
* The entry ID and external URL captured from the Accounting System response, ready to be reported back to Pleo in the Export workflow
#### Why It Matters
Posting behaviour determines whether entries immediately impact financial statements or are held for review first. Draft posting gives bookkeepers the opportunity to verify entries before they affect reporting, particularly valuable during initial integration setup, after configuration changes, or when processing large batches. Defaulting to draft when configuration is missing prevents unintended immediate posting.
#### Integration Design
If you're an integration developer or architect, read the [Posting Behaviour](/docs/current/integration-design/exports/integration-design-exports-posting-behaviour) integration design doc before implementing this step. It covers the supported posting states, how to map them to Accounting System equivalents, the default behaviour rules, and the implementation order within the export workflow.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying How-to article.
How to Apply Posting Behaviour
***
## Result
After completing this workflow for all Export Items:
* Accounting entries exist in the Accounting System for each successfully processed Export Item
* Entries are balanced, correctly mapped, and assigned to the correct periods
* Attachments are linked to their corresponding entries where supported
* Each entry reflects the configured posting status (draft or finalised)
* A success or failure result has been captured for every Export Item, ready to be reported back to Pleo
Export Items are now ready for status reporting and Export Job completion in the Export Integration Workflow.
***
## What Comes Next?
Return to the Export Integration Workflow to report Export Item outcomes and close the job:
* [Update Export Items](/docs/current/guides/export-integration-workflow-guide#6-update-export-items)
* [Update and Complete Export Job](/docs/current/guides/export-integration-workflow-guide#7-update--complete-export-job)
***
## Related Reading
* [Exports Overview](/docs/current/integration-design/exports/integration-design-exports-overview)
* [Exports Lifecycle](/docs/current/platform/exports/lifecycle)
***
# OAuth 2.0 Access to Staging Workflow Guide
Source: https://developers.pleo.io/docs/current/guides/environment-access/staging-oauth-workflow
This guide applies **only to Pleo Partners**. All partner integrations must use **OAuth 2.0** as their authentication method.
This workflow explains how **Pleo Partners** gain access to **Pleo’s Staging environment** and prepare their integration to authenticate using **OAuth 2.0**.
It focuses on **partner onboarding, environment access, and readiness**, not the OAuth 2.0 protocol itself. By the end, you’ll understand **what must be in place before your integration can authenticate and call Pleo APIs**.
## What You’ll Have Built
After completing this workflow, as a Pleo Partner:
* You have access to Pleo’s **Staging environment** with dummy data
* An OAuth 2.0 client is registered for your integration
* You have received an **OAuth 2.0 Client ID and Client Secret**
* You are ready to complete the OAuth 2.0 workflow and make authenticated API calls in Staging
## Who This Guide Is For
This guide is intended **exclusively for Pleo Partners** building **multi-customer integrations** with Pleo using **OAuth 2.0**.
It is not intended for Pleo customers building internal tools or single-company integrations.
The guide assumes:
* You do not yet have Staging access
* You are onboarding as a partner and preparing an integration for review
## Before You Start
You should be familiar with:
* Pleo’s role as an [Expense Management Solution](/docs/current/getting-started/platform-overview)
* The Pleo Partner [Early Access Programme (EAP)](/docs/current/getting-started/developer-partnership-programme)
* [OAuth 2.0 Overview](/docs/current/authentication/oauth/oauth-overview)
## Access to Staging Workflow Steps
### 1. Get Access to the Staging Environment (Partners Only)
**Purpose:**
Before you can build or test your integration, you must have access to **Pleo’s Staging environment**.
Staging mirrors production behaviour but uses dummy data, allowing **Pleo Partners** to develop and test integrations safely.
Access is granted as part of the **partner onboarding process** and includes registering your integration intent through the **Early Access Programme (EAP)**.
**Input:**
* Partner or company details
* Integration use case and intended API usage
**Output:**
* Login access to Pleo’s Staging environment
* A Staging company populated with dummy data
At this stage, your integration is authorised to request OAuth credentials but cannot yet call Pleo APIs.
**Integration Design / Why It Matters:**
* Staging access is required before OAuth credentials can be issued
* Dummy data ensures no real financial or accounting data is exposed
* EAP registration allows Pleo to review and support your integrations
→
***
### 2. Complete OAuth 2.0 Setup
**Purpose:**
After gaining Staging access, your integration must **successfully complete the OAuth 2.0 authorisation flow** before it can call any APIs.
This ensures your integration can **authenticate securely** and obtain valid access tokens.
The `Authorization: Bearer ` header must contain a valid OAuth 2.0 **access token** issued by Pleo. This is **not** the Client Secret and cannot be used until the OAuth 2.0 workflow has been completed.
**How This Step Is Completed:**
You can complete OAuth 2.0 setup in one of two ways, depending on how you are building and testing your integration:
* [**Option A:**](/docs/current/guides/environment-access/staging-oauth-workflow#option-a-full-oauth-2-0-workflow-production-style-integration) Full OAuth 2.0 Workflow (Production-style Integration)
* [**Option B:**](/docs/current/guides/environment-access/staging-oauth-workflow#option-b-oauth-2-0-using-postman) OAuth 2.0 using Postman
**Outcome of This Step:**
By completing the linked guide, you will have:
* A valid OAuth 2.0 **access token** issued by Pleo
* Confirmation that your integration can authenticate successfully and call APIs within approved scopes
**Why This Matters:**
* OAuth 2.0 access tokens replace user credentials
* Tokens are scoped and time-limited to maintain security
#### Option A: Full OAuth 2.0 Workflow (Production-style Integration)
You'll cover:
* Directing users to the authorisation endpoint
* Exchanging authorisation codes for access tokens
* Handling refresh tokens and token expiry
* Making your first API call
→
***
## What Comes Next?
Once Staging access is confirmed and your integration can successfully call Pleo APIs using OAuth 2.0, you can continue with:
* **Export Expenses Workflow Guide** (coming soon)
# Export Integration Workflow Guide
Source: https://developers.pleo.io/docs/current/guides/export-integration-workflow-guide
This guide covers [Integration Level 1](/docs/current/getting-started/accounting-integrations-overview), which enables exporting expenses and attachments. It is suitable for basic accounting workflows.
## What You'll Have Built
After implementing this workflow:
* Export Jobs are detected and processed reliably, with sequential ordering guaranteed.
* Expenses are validated, mapped, and recorded correctly in the Accounting System.
* Attachments and bookkeeping logic are handled consistently for every Export Item.
* Export outcomes, both success and failure, are clearly reported back to Pleo for each item.
* The integration aligns with Pleo's export guarantees and bookkeeping model.
## Who This Guide Is For
This guide is intended for:
* Integration developers
* Solution architects
* Accounting platform integrators
It focuses on **workflow understanding**, not implementation details.
## Before You Start
You should be familiar with:
* Pleo's [supported authentication](/docs/current/integration-design/auth/integration-design-auth-overview#authentication-policy-overview) methods.
* The [Export Lifecycle](/docs/current/platform/exports/lifecycle)
## Export Workflow Overview
When a bookkeeper queues expenses in Pleo's Web App and clicks Export, Pleo creates an Export Job, a batch of Export Items (individual expenses), ready to be processed by the integration. The integration detects this job, verifies the Accounting System is ready, retrieves the items, and processes each one into an accounting entry. Once all items are processed, the integration reports the outcome for each item back to Pleo and sends a final event to close the job.
Steps 1–4 and 6–7 interact directly with Pleo's Export API. Step 5 (Process & Record Export Items) takes place inside the Accounting System or ERP and is covered in detail in the [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide). Each step produces outputs that the next step depends on.
***
## Steps
### 1. Detect and Start Export Jobs
#### Purpose
When Pleo creates an Export Job, the integration must detect it and mark it as started before any processing can begin. Detection can happen via a webhook notification (preferred, as Pleo triggers the integration the moment a job is created) or via scheduled polling. In either case, the integration selects the oldest pending job and sends a `started` event to Pleo's Export API, which transitions the job to `in_progress` and establishes the integration's exclusive processing rights.
On startup or after a connection loss, the integration may also detect an `in_progress` job it previously started. In this case, the `started` event is skipped and processing resumes from where it left off.
#### Input
* An Export Job created by Pleo when expenses are queued and exported via the Web App, with status `pending`
* A webhook notification (`export-job.created`) or a scheduled polling trigger that initiates detection
* On recovery: an Export Job already in `in_progress` state that was previously started by this integration but not completed
#### Workflow Process
#### Output
* The oldest eligible Export Job identified and selected for processing
* The Export Job marked as `in_progress` via the `started` event, confirming the integration has taken responsibility for this batch
* No other worker or integration instance will start the same job
#### Why It Matters
Export Jobs must be processed one at a time and in order to prevent accounting inconsistencies. Starting a job is the mechanism that establishes this exclusivity: only one integration instance can successfully send the `started` event for a given job. This prevents duplicate accounting entries from being created if multiple workers are briefly running simultaneously, and creates an auditable record of when processing began.
#### Integration Design
If you're an integration developer or architect, read the [Detect & Start Export Jobs](/docs/current/integration-design/exports/integration-design-exports-detect-and-start-export-jobs) integration design doc before implementing this step. It covers the detection mechanisms (webhook, polling, ad hoc trigger), sequencing requirements, how concurrent start conflicts are handled, and the rules for recovery scenarios.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying How-to article.
How to Detect and Start Export Jobs for Processing
***
### 2. Perform Pre-Export Validation
#### Purpose
Before any Export Items are processed, the integration verifies that the Accounting System is reachable and correctly configured for this Export Job. This check happens immediately after the job is started and before any accounting data is touched. If the Accounting System is unavailable, the integration is misconfigured, or the required bookkeeping method is not supported, the job is marked as failed with a clear, actionable reason, and processing stops cleanly.
#### Input
* The started Export Job (status `in_progress`), including metadata such as `companyId` and `vendorBasedBookkeeping`
* A valid connection to the target Accounting System, with credentials and required permissions in place
#### Workflow Process
#### Output
* Validation passed: the integration proceeds to retrieve Export Items for processing
* Validation failed: the Export Job is marked `failed` with a specific `failureReason` and `failureReasonType`, and processing stops. The failure is surfaced to the user in Pleo's Web App.
#### Why It Matters
Failing early is far less costly than failing mid-export. If validation were skipped and a problem were discovered halfway through processing, the Accounting System could be left in an inconsistent state, with some entries created and others missing. Pre-export validation ensures the integration either proceeds with confidence or fails cleanly before any accounting data is written.
#### Integration Design
If you're an integration developer or architect, read the [Perform Pre-Export Validation](/docs/current/integration-design/exports/integration-design-exports-pre-export-validation) integration design doc before implementing this step. It covers the required validation checks, the supported failure reason types, and when to fail the job versus when to handle issues at the item level.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying How-to article.
How to Perform Pre-Export Validation
***
### 3. Retrieve Export Job Items (Control Layer)
#### Purpose
Before fetching full accounting payloads, the integration retrieves a lightweight control list of all Export Job Items. Each item includes its unique `accountingEntryId` and current processing status (`pending`, `in_progress`, `successful`, or `failed`), but not the full expense data. This establishes the processing scope: which items exist in this job and which still require work.
The control layer is also the recovery mechanism. If the integration restarts mid-export, it re-fetches this list, filters for items still in `pending` state, and resumes from where it left off without reprocessing already-completed items. Items in `in_progress` state are intentionally excluded from the recovery filter. They may have been partially processed before the crash, and retrying them risks creating duplicate accounting entries.
#### Input
* The Export Job ID (`jobId`) from the started Export Job
* On recovery: awareness that the job is already `in_progress`, triggering a re-fetch to determine remaining work
#### Workflow Process
#### Output
* A complete, paginated list of all Export Job Items with their current statuses
* A defined processing scope: the set of items still requiring work, typically those with status `pending`
* A deterministic processing order established, ensuring consistent and auditable execution across retries
#### Why It Matters
Separating the control layer from the data layer keeps the workflow modular and resilient. The integration knows exactly what it needs to process before making heavier API calls for full payloads. This prevents partial processing, enables the workflow to resume safely after interruptions, and ensures no item is missed or inadvertently processed twice.
#### Integration Design
If you're an integration developer or architect, read the [Retrieve Export Job Items](/docs/current/integration-design/exports/integration-design-exports-fetch-export-items-control-layer) integration design doc before implementing this step. It covers the control layer vs data layer distinction, pagination requirements, the recovery strategy, and how deterministic ordering should be implemented.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying How-to article.
How to Retrieve Export Job Items
***
### 4. Fetch Export Item Data (Data Layer)
#### Purpose
With the processing scope established, the integration retrieves the full accounting payload for each Export Item. This is the data layer: each item contains all the information needed to create an accounting entry: amounts, dates, GL account codes, vendor details, tax codes, dimensions, attachment references, and the bookkeeping method to apply.
This step is intentionally separate from the control layer. The integration first knows what to process (Step 3), then retrieves the data needed to process it (Step 4), keeping workflow control and data retrieval cleanly decoupled.
#### Input
* The Export Job ID (`jobId`)
* The list of `accountingEntryId` values from the control layer (Step 3), defining which items require full data retrieval
#### Workflow Process
#### Output
* Full accounting payloads retrieved for each Export Item in the processing scope
* Each payload validated to confirm it contains the data required for the intended bookkeeping method
* Items ready to be passed into the Accounting System processing workflow
#### Why It Matters
The Export Item payload contains everything the integration needs to create a correct accounting entry. Fetching it as a separate step, after the processing scope is established, means the integration can validate completeness before committing to any accounting operations. It also means items can be handled individually, so a problem with one item's data does not prevent others from being processed.
#### Integration Design
If you're an integration developer or architect, read the [Fetch Export Item Data](/docs/current/integration-design/exports/integration-design-exports-fetch-export-items-data-layer) integration design doc before implementing this step. It covers the data layer structure, the fields available in the Export Item payload, pagination, validation requirements, and how to handle recovery.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying How-to article.
How to Fetch Export Item Data
***
### 5. Process & Record Export Items
#### Purpose
This step is where the integration processes each Export Item and records it into the Accounting System. For each item, the integration resolves the bookkeeping method, applies accounts and data mapping, handles attachments, assigns the correct accounting period, and creates the entry using the configured posting behaviour. Each Export Item is processed independently: a failure on one item does not stop the others from being processed.
#### Input
* Full Export Item payloads from Step 4
* Integration configuration: accounts mapping, posting behaviour, and closed period strategy
* A valid connection to the Accounting System
#### Workflow Process
#### Output
* Draft or finalised accounting entries created in the Accounting System for each successfully processed item
* Attachments uploaded and linked to their corresponding accounting entries where applicable
* A success or failure result captured for each item, ready to be reported back to Pleo in Step 6
#### Why It Matters
This is where Pleo expense data becomes accounting records. Getting this step right, with correct GL accounts, accurate data mapping, and appropriate posting status, determines the quality and auditability of the Accounting System. Per-item failure isolation ensures that a single problematic expense does not block the rest of the batch.
#### Step-by-Step Instructions
This step is covered in full in the AS/ERP Processing Workflow Guide, which walks through bookkeeping method resolution, accounts mapping, data mapping, attachment handling, accounting period assignment, and posting behaviour.
AS/ERP Processing Workflow Guide
***
### 6. Update Export Items
#### Purpose
After each Export Item is processed in the Accounting System, the integration reports the outcome back to Pleo. For successful items, this includes the external ID and URL of the entry created in the AS, providing a direct link between the Pleo expense and its accounting record. For failed items, this includes a clear, actionable failure reason. Pleo uses these updates to surface results to bookkeepers in the Export Queue.
Items are sent in batches of up to 100 per API request.
#### Input
* The success or failure result for each processed Export Item, captured during Step 5
* The `accountingEntryId` for each item, from the control layer (Step 3)
* For successful items: the external ID and URL assigned by the Accounting System
* For failed items: the failure reason and reason type, derived from the Accounting System's response
#### Workflow Process
#### Output
* Every Export Item in the batch updated with a final status: `successful` or `failed`
* Successful items linked to their corresponding entries in the Accounting System via external ID and URL
* Failed items recorded with actionable error information, visible to bookkeepers for review and correction
#### Why It Matters
Reporting item-level outcomes back to Pleo is what makes the export visible and auditable for users. Without these updates, bookkeepers cannot see which expenses exported successfully, which failed, or why. Accurate and timely updates also enable Export Job completion: the final step cannot be triggered until all items have a recorded outcome.
#### Integration Design
If you're an integration developer or architect, read the [Update Export Items](/docs/current/integration-design/exports/integration-design-exports-update-export-items) integration design doc before implementing this step. It covers the supported status values, the required and optional fields for successful and failed items, and the failure reason type mappings for common Accounting System errors.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying How-to article.
How to Update Export Items
***
### 7. Update & Complete Export Job
#### Purpose
Once all Export Items have been updated with their final statuses, the integration sends a completion event to close the Export Job. The event reflects the aggregated outcome of the entire batch: `completed` if all items succeeded, `completed_with_errors` if some failed, or `failed` if all items failed. This is the signal to Pleo that processing is finished and the full export result can be surfaced to users.
#### Input
* The final processing status of all Export Items, established in Step 6
* The Export Job ID (`jobId`)
#### Workflow Process
#### Output
* The Export Job marked with its final status: `completed`, `completed_with_errors`, or `failed`
* Export results fully visible to users in Pleo's Export Queue, including per-item success and failure details
* The export lifecycle complete: no further updates can be made to this job
#### Why It Matters
Completing the Export Job is the final confirmation that the integration has finished processing the batch. It unlocks the user-visible export status in Pleo's Web App, allowing bookkeepers to review results, investigate failures, and resubmit if necessary. It also provides a clear audit trail of when processing ended and what the overall outcome was for the batch.
#### Integration Design
If you're an integration developer or architect, read the [Update & Complete Export Job](/docs/current/integration-design/exports/integration-design-exports-update-and-complete-export-job) integration design doc before implementing this step. It covers the supported completion events, the conditions under which each is used, and the failure reason guidance for job-level failures.
#### Step-by-Step Instructions
When you're ready to start implementing, follow the step-by-step instructions in the accompanying How-to article.
How to Update & Complete the Export Job
***
## What Comes Next?
After implementing the Export Expenses workflow, you can extend your integration with:
* [Tags Sync](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-overview)
***
## Related Reading
* [Exports Overview](/docs/current/integration-design/exports/integration-design-exports-overview)
* [Exports Lifecycle](/docs/current/platform/exports/lifecycle)
***
# OAuth 2.0 Setup Workflow Guide (Manual Token Lifecycle)
Source: https://developers.pleo.io/docs/current/guides/oauth-workflow-guide
Pleo Partners building **multi-customer integrations** must implement **OAuth 2.0** as the recommended authentication method.
This workflow outlines how third-party applications integrate securely with Pleo using OAuth 2.0. Each step explains **what happens, why it matters, and the expected result**, while linking to detailed how-to guides for implementation.
By following this guide, developers and architects can understand **how the pieces fit together**, ensuring a secure and reliable integration.
## What You'll Have Built
After completing the workflow:
* An OAuth 2.0 client is registered and authorised for your integration.
* Access and refresh tokens are obtained, stored securely, and refreshed correctly.
* Your integration can interact with Pleo APIs safely, respecting scopes and user consent.
* The OAuth flow is resilient to token expiry, revocation, and race conditions.
Registering an OAuth 2.0 client creates **authentication credentials** only. It does **not** create or publish a Marketplace app.
## Who This Guide Is For
This guide is intended for developers and architects implementing OAuth 2.0 integrations with Pleo APIs. It focuses on **workflow understanding**, not step-by-step coding details, those are in the linked how-to guides.
## Scope
All instructions assume:
* An OAuth 2.0 integration with Pleo APIs
* Integration with the **Staging environment**
## OAuth 2.0 Workflow Steps
### 1. Register OAuth Client
**Purpose:**\
Registering an OAuth 2.0 client for your integration ensures it can request access to user resources securely. This step provides the credentials and configuration needed to start the OAuth flow.
**Input:**
* Integration details (name, URI, logo, terms, privacy policy)
* Redirect URIs and scope requirements
**Output:**
* A **Client ID** and **Client Secret**
* Defined redirect URIs registered with Pleo
* OAuth 2.0 client ready for user authorisation
**Integration Design / Why It Matters:**
* Client credentials authenticate your integration via its OAuth client to the Pleo authorisation server
* Redirect URIs and scopes ensure requests are valid and secure
* See: [OAuth 2.0 Client Registration & Configuration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-registration)
→
***
### 2. Direct Users to Authorisation Endpoint
**Purpose:**\
Redirecting users to Pleo ensures they explicitly grant your integration permission to access their data. This step is required before any tokens are issued.
**Input:**
* The **Client ID** from the previous step
* PKCE values (`code_verifier` / `code_challenge`)
**Output:**
* Users see the Pleo consent screen and approve or deny access
* An **authorisation code** is issued upon approval
**Integration Design / Why It Matters:**
* PKCE prevents authorisation code interception
* The `state` parameter protects against CSRF attacks
* Scopes define what data your integration can access
* See:
* [PKCE and Secured Patterns](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-pkce-and-secured-patterns)
* [OAuth 2.0 Client Registration - Details and Technical Requirements](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-registration)
→
***
### 3. Handle Redirect & Exchange Authorisation Code
**Purpose:**\
Exchange the short-lived authorisation code for access and refresh tokens securely. This step moves credentials from user consent to usable tokens for API access.
**Input:**
* **Authorisation code** returned from Pleo
* Stored **code\_verifier** (for PKCE)
**Output:**
* Access and refresh tokens issued and stored securely on your backend
**Integration Design / Why It Matters:**
* Token exchange must be server-to-server to keep secrets safe
* PKCE verification ensures the code wasn’t intercepted
* See: [PKCE and Secured Patterns](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-pkce-and-secured-patterns)
→
***
### 4. Use Access Token to Call APIs
**Purpose:**\
Access tokens authenticate your integration when interacting with Pleo APIs, eliminating the need for user credentials on each request.
**Input:**
* Access token obtained in the previous step
**Output:**
* API endpoints respond with requested data or action confirmation (JSON payloads, status codes)
**Integration Design / Why It Matters:**
* Always send tokens in the `Authorization` header over HTTPS
* Ensure required scopes match endpoint permissions
* Token misuse or exposure could compromise security
* See: [Token Lifecycle Overview](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview)
→
***
### 6. Handle Token Expiry or Revocation
**Purpose:**\
Refresh tokens can expire or be revoked. Detecting and handling these events ensures your integration fails safely and can guide users to reauthenticate.
**Input:**
* API errors or failed refresh attempts indicating invalid or revoked tokens
**Output:**
* Integration stops API calls using invalid tokens
* Users are prompted to reauthenticate
* Newly issued tokens replace invalid ones
**Integration Design / Why It Matters:**
* Clear recovery flow prevents broken sessions
* Logging failures aids troubleshooting
* See: [Handling Refresh Token Expiry or Revocation](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-refresh-token-expiry-revocation)
→
***
## What Comes Next?
Once Staging access is confirmed and your integration can successfully call Pleo APIs using OAuth 2.0, you can continue with:
* **Export Expenses Workflow Guide** (coming soon)
***
## FAQs
These terms describe different parts of how OAuth 2.0 works in the Pleo platform.
The difference between an **integration**, **application**, and **OAuth 2.0 client** is explained in the [OAuth 2.0 Concepts and Terminology](/docs/current/authentication/oauth/oauth-overview#concepts-and-terminology) section.
***
## Related Reading
* [OAuth 2.0 Overview](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview)
***
# OAuth 2.0 Setup with Postman
Source: https://developers.pleo.io/docs/current/guides/oauth-workflow-guide-postman
Pleo Partners building **multi-customer integrations** must implement **OAuth 2.0** as the recommended authentication method. They may use Postman for initial testing.
This workflow explains how to use Postman to integrate with Pleo using OAuth 2.0.
Each step explains **what happens, why it matters, and the expected result**, while linking to detailed how-to guides for implementation.
Using Postman allows you to test and validate the OAuth 2.0 flow without implementing custom token lifecycle management, while still understanding how the full authorisation sequence works.
If you want to manage the token lifecycle yourself, see the [**OAuth 2.0 Setup Workflow Guide**](/docs/current/guides/oauth-workflow-guide).
## What You'll Have Built
After completing this workflow:
* Your integration is registered with Pleo as an OAuth 2.0 client
* You have securely received your **Client ID** and **Client Secret**
* Postman is configured to automatically retrieve and refresh **access tokens**
* You can successfully call Pleo APIs in the Staging environment
Registering an OAuth 2.0 client creates **authentication credentials** only. It does **not** create or publish a Marketplace app.
## Who This Guide Is For
This guide is intended for developers and architects implementing OAuth 2.0 integrations with Pleo APIs.
It focuses on understanding the OAuth 2.0 workflow and configuring Postman correctly. The focus is **not** on implementing production-grade token handling.
## Scope
All instructions assume:
* An OAuth 2.0 integration with Pleo APIs
* Integration with the **Staging environment**
## OAuth 2.0 Workflow Steps
## 1. Register OAuth 2.0 Client (Postman)
### Purpose
Registering an OAuth 2.0 client for your integration enables it to participate in the OAuth 2.0 flow.
### Input
* Integration details (name, URI, terms, privacy policy)
* Postman redirect URI
* Required API scopes
### Output
* A **Client ID** and **Client Secret**
* OAuth 2.0 client authorised to initiate OAuth flows using Postman
### Why This Matters
* The **Client ID** identifies your **OAuth 2.0 client** to Pleo.
* The **Client Secret** authenticates your **OAuth 2.0 client** securely.
* The Postman registered redirect URIs prevent unauthorised token interception.
* Defined API scopes restrict access to approved resources only.
→
***
## 3. Make a Pleo API Call Using Postman
### Purpose
Send authorised API requests using the Access Token retrieved by Postman.
### Input
* A configured OAuth 2.0 token in Postman
* A valid Pleo API endpoint URL (Staging)
* Required HTTP method (e.g., GET)
### Output
* An authorised HTTP request
* JSON response from the Pleo API
* Confirmation that OAuth 2.0 configuration works end-to-end
### Why This Matters
* Confirms your OAuth 2.0 setup is correct.
* Validates scopes and consent.
* Ensures tokens are being correctly injected into requests.
→
***
## What Comes Next?
Once API calls succeed:
* Test additional endpoints
* Validate different scope combinations
* Move from Staging to Production when ready
* Or implement full OAuth token handling in your own backend
***
## FAQs
These terms describe different parts of how OAuth 2.0 works in the Pleo platform.
The difference between an **integration**, **application**, and **OAuth 2.0 client** is explained in the [OAuth 2.0 Concepts and Terminology](/docs/current/authentication/oauth/oauth-overview#concepts-and-terminology) section.
***
## Related Reading
* [OAuth 2.0 Overview](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview)
* [OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)
***
# Standalone API Key Workflow Guide
Source: https://developers.pleo.io/docs/current/guides/standalone-api-keys-workflow-guide
Standalone API Keys are [**not available to all customers**](/docs/current/authentication/standalone-api-keys-overview).
This guide walks through the **end-to-end workflow** for using Standalone API Keys with Pleo APIs. You'll be using **cURL** to make the API calls.
If you prefer to test with Postman:
* Follow Step 1 of this guide to create your Standalone API Key, then
* Follow the [How to Make an API Call Using a Standalone API Key (Postman)](/docs/current/how-tos/api-keys/how-to-make-an-api-call-using-standalone-api-keys-postman) article to configure Postman and make your first API call.
## Before You Start
You must have:
* [Access to the **Pleo Web App**](/docs/current/how-tos/environment-access/how-to-get-access-to-all-env-standalone-api-keys) in **Staging** or **Production**
* **Admin** or **Bookkeeper** permissions
If you do not see **API Keys** under Settings, Standalone API Keys are not enabled for your organisation.
## What You’ll Have Built
By the end of this workflow:
* You have generated a Standalone API Key
* You understand environment and permission scoping
* You have successfully authenticated against the Pleo API
## Workflow Steps
### 1. Generate Your Standalone API Key
**Purpose:** Create an API Key in the Pleo Web App for your chosen environment.
**Input:** No inputs. See [Before You Start](#before-you-start) section.
**Output:** A scoped **API Key** usable for authenticating API requests.
**Integration Design / Why It Matters:**
* Standalone API Keys authenticate API requests and are [scoped by environment, company, and permitted API scopes](/docs/current/authentication/standalone-api-keys-overview#permission-scoped-access).
* Always store API Keys [securely](/docs/current/integration-design/auth/api-keys/integration-design-security-for-standalone-api-keys) after creation. Never expose them publicly or embed them in client-side code.
If you plan to build a production integration, review the [Integration Design for Standalone API Keys](/docs/current/integration-design/auth/api-keys/integration-design-for-standalone-api-keys) page to ensure correct lifecycle and security handling.
→
***
### 2. Authenticate Your First API Request
**Purpose:** Confirm API connectivity using your Standalone API Key.
**Input:**
* API Key (authentication header)
* `company_id` associated with your Pleo company
Every request must include the correct **company\_id** to ensure proper scoping.
**Output:** A successful API response confirms your key is valid and your requests are correctly scoped.
→
***
## Result
After completing this guide:
* You have a valid Standalone API Key
* You have confirmed API connectivity
* You have a working authenticated setup
***
## What Comes Next?
* Test additional Pleo API endpoints using Standalone API Keys
* Move to OAuth 2.0 if building multi-customer integrations:
* [OAuth 2.0 Setup with Postman Workflow Guide](/docs/current/guides/oauth-workflow-guide-postman)
* [OAuth 2.0 Setup Workflow Guide (Manual Token Lifecycle)](/docs/current/guides/oauth-workflow-guide)
***
## Related Reading
* [Integration Design for Standalone API Keys](/docs/current/integration-design/auth/api-keys/integration-design-for-standalone-api-keys)
* [User Experience Guidelines for Standalone API Keys](/docs/current/integration-design/auth/api-keys/integration-design-user-exp-guidelines-for-standalone-api-keys)
* [Security and Credential Management for Standalone API Keys](/docs/current/integration-design/auth/api-keys/integration-design-security-for-standalone-api-keys)
***
# How to Apply Data Mapping
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-apply-data-mapping-for-as-erp-processing
Data Mapping transforms Export Item data into the **structured fields required by your Accounting System (AS/ERP).**
This step ensures that all required financial, contextual, and reporting data is correctly transferred.
## Prerequisites
Before you begin:
* You've [fetched the export item payload data](/docs/current/how-tos/accounting-integrations/how-to-fetch-export-item-data-for-as-erp-processing)
* You've completed [accounts mapping](/docs/current/how-tos/accounting-integrations/how-to-determine-accounts-mapping-for-as-erp-processing)
* You're familiar with the [Integration Design for data mapping](/docs/current/integration-design/exports/integration-design-exports-data-mapping)
## Mapping Order
Mappings must be applied in the following order:
1. Mandatory datapoints
2. Invoice datapoints (if applicable)
3. Optional datapoints
4. VAT handling
Apply mappings sequentially to avoid overwriting required accounting data.
## Relevant Export Item Fields
```json theme={null}
{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"accountingEntryId": "59540ed2-0d68-4e36-9e31-58223975d9e9",
"type": "card_purchase",
"subType": null,
"date": "2025-12-10T15:46:34Z",
"amount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"note": "Printer ink",
"files": [
{
"url": "",
"type": "image/jpeg",
"size": 13010
}
],
"supplier": {
"code": "1340472473",
"name": "Target",
"categoryCode": "1000",
"country": "GB",
"account": null,
"taxIdentifier": null
},
"user": {
"id": "6b71f6bd-e83d-4d49-88ee-2b8cda2d57cf",
"name": "Luke Richardson",
"code": null
},
"team": {
"id": "747aaf60-56c6-4b46-ad92-f8a0cb59cf8b",
"code": "5678",
"name": "Engineering"
},
"accountingEntryLines": [
{
"accountingEntryLineId": "0c76ea71-aaaa-4ece-bb68-e1166ccaea04",
"lineAmount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"netAmount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"account": {
"id": "7966c3ba-e4de-4574-8604-6cfa48d62cc8",
"code": "6990000",
"name": "Printing & Stationary",
"identifier": "6990000"
},
"tax": {
"id": "997d8526-5872-484d-ba07-c7a07e08e555",
"code": "0001",
"type": "inclusive",
"amount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 0
},
"inWalletCurrency": {
"currency": "GBP",
"value": 0
}
},
"rate": 0.00
},
"tags": []
}
],
"additionalInformation": {
"reconciliationId": "2500001",
"reconciledEntries": null,
"attendees": [],
"invoiceInformation": null
},
"bookkeeping": {
"method": "journal"
},
"vendor": {
"id": "22e1f2c9-1360-4291-ab41-6b23dcea8888",
"name": "TestVendor",
"code": "acc1234",
"externalId": "ext12345",
"registrationNumber": "reg001234",
"taxRegistrationNumber": "taxreg1234",
"country": "UK",
"defaultCurrency": "GBP"
},
"contraAccount": {
"id": "993d664c-9b7c-4efc-a677-510e69200857",
"code": "0876000",
"name": "0876000_ChartAccounts",
"identifier": "0876000"
},
"_links": {
"web": {
"exportItem": "https://app.staging.pleo.io/export/export-item/0c76ea71-aaaa-4ece-bb68-e1166ccaea04"
}
},
"servicePeriod": null
},
# [other Export Items omitted for brevity]
# [Pagination omitted for brevity]
```
### Supplier
```json theme={null}
"supplier": {
"name": "GOOGLE*ADS8693645259",
"country": "DK"
}
```
### User & Team
```json theme={null}
"user": { "name": "John Doe" },
"team": { "name": "Marketing" }
```
### Notes
```json theme={null}
"note": "Printer ink"
```
### Tags
```json theme={null}
"tags": [
{ "code": "" }
]
```
## Steps
### 1. Map Mandatory Datapoints
These fields must always be mapped.
Typical mappings:
```pseudo theme={null}
entry.account = mappedExpenseAccount
entry.amount = accountingEntryLines[].lineAmount.inWalletCurrency.value
entry.currency = accountingEntryLines[].lineAmount.inWalletCurrency.currency
entry.date = date
entry.taxCode = accountingEntryLines[].tax.code
entry.description = buildDescription()
entry.dimensions = tags
entry.reconciliationId = additionalInformation.reconciliationId
```
Include:
* Expense account (from Accounts Mapping)
* Amount (wallet currency)
* Currency
* Date
* Tax code
* Tags / dimensions
* Reconciliation data
### 2. Map Invoice Datapoints (if applicable)
Apply only if:
```pseudo theme={null}
type == "invoice" OR type == "invoice_payment"
```
Mappings:
```pseudo theme={null}
entry.invoiceNumber = additionalInformation.invoiceInformation.invoiceNumber
entry.invoiceDate = additionalInformation.invoiceInformation.invoiceDate
entry.dueDate = additionalInformation.invoiceInformation.dueDate
entry.paymentDate = additionalInformation.invoiceInformation.paymentDate
```
Currency handling:
* `invoice` → supplier currency
* `invoice_payment` → wallet currency
Provide exchange rates if required by the Accounting System.
### 3. Map Optional Datapoints
Optional fields enhance reporting.
Examples:
```pseudo theme={null}
entry.netAmount = accountingEntryLines[].netAmount.inWalletCurrency.value
entry.taxAmount = accountingEntryLines[].tax.amount.inWalletCurrency.value
entry.team = team.code
entry.employee = user.code
entry.memo = note
```
Optional fields must never overwrite mandatory fields.
### 4. Apply VAT Handling Rules
Determine whether to send VAT explicitly.
```pseudo theme={null}
if accountingSystem.calculatesVAT:
do not send VAT amount
else:
send VAT amount from Pleo
```
### 5. Build Final Entry Structure
Combine all mapped data into the final structure expected by your Accounting System.
```pseudo theme={null}
entry = {
accounts: mappedAccounts,
amounts: mappedAmounts,
metadata: mappedFields
}
```
## Results
After completing this step:
* All required data fields are mapped
* Optional enhancements are included where supported
* Entries are complete and ready for posting
***
## What Comes Next?
How to Transfer Attachments
***
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# How to Apply Posting Behaviour
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-apply-posting-behaviour-for-as-erp-processing
Posting behaviour determines whether accounting entries are created as **draft** or **finalised** in the Accounting System.
## Prerequisites
Before you begin:
* You've [fetched the export item payload data](/docs/current/how-tos/accounting-integrations/how-to-fetch-export-item-data-for-as-erp-processing)
* You've completed:
* [Accounts Mapping](/docs/current/how-tos/accounting-integrations/how-to-determine-accounts-mapping-for-as-erp-processing)
* [Data Mapping](/docs/current/how-tos/accounting-integrations/how-to-apply-data-mapping-for-as-erp-processing)
* [Attachment Handling](/docs/current/how-tos/accounting-integrations/how-to-transfer-attachments-for-as-erp-processing)
* [Accounting Period Assignment](/docs/current/how-tos/accounting-integrations/how-to-assign-accounting-periods-for-as-erp-processing)
* You're familiar with the [Integration Design for posting behaviour](/docs/current/integration-design/exports/integration-design-exports-posting-behaviour)
## Steps
### 1. Read Posting Configuration
The Export API does not provide posting behaviour.
If posting behaviour is required, it must be defined as an **integration configuration**.
```pseudo theme={null}
postingMode = config.postingMode // "draft" or "finalised"
```
Posting behaviour is **not** provided by Pleo. It must be implemented and controlled by the integration.
### 2. Apply Posting Behaviour
Based on the integration configuration, determine whether the entry should be created as draft or finalised:
| Integration configuration | Required behaviour |
| ------------------------- | --------------------------- |
| Draft status enabled | Create entries as draft |
| Draft status disabled | Create entries as finalised |
```pseudo theme={null}
if config.draftStatusEnabled:
postingMode = "draft"
else:
postingMode = "finalised"
```
### 3. Map to Accounting System Status
Accounting Systems may represent posting status differently.
Common equivalents:
* draft vs posted
* approved vs unapproved
* saved vs committed
The integration must translate the configured posting mode into the closest equivalent supported by the Accounting System.
```pseudo theme={null}
asStatus = mapPostingModeToAS(postingMode)
```
### 4. Apply Default Behaviour
If posting configuration is missing or cannot be determined:
```pseudo theme={null}
postingMode = "draft"
```
Defaulting to draft allows users to review entries before they impact financial reporting.
### 5. Create Entry and Capture Result
Once posting behaviour has been applied, the integration must create the accounting entry in the Accounting System.
```pseudo theme={null}
result = accountingSystem.createEntry(mappedEntry, postingStatus)
```
The integration must capture the result of this operation:
* success (entry created)
* failure (entry rejected)
If the operation fails:
* capture the error message from the Accounting System
## Result
After completing this step:
* Posting behaviour has been applied (draft or finalised)
* The accounting entry has been created in the Accounting System
* The success or failure status of the operation has been captured
* Any error messages are available for downstream handling
***
## What Comes Next?
At this point, AS/ERP processing is complete for the current Export Item.
Repeat the AS/ERP processing steps for all remaining Export Items and capture the success or failure result for each item immediately. This reduces the risk of data inconsistency in scenarios such as connection loss or partial failures.
Reporting Export Item statuses back to Pleo (next step) must be performed in batches of up to 100 items per API request.
Export Integration Workflow: Step 6: Update Export Items
***
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# How to Assign Accounting Periods
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-assign-accounting-periods-for-as-erp-processing
Accounting period assignment ensures Export Items are posted into the correct financial period according to both Pleo configuration and Accounting System rules.
## Prerequisites
Before you begin:
* You've [fetched the export item payload data](/docs/current/how-tos/accounting-integrations/how-to-fetch-export-item-data-for-as-erp-processing)
* You've completed:
* [Accounts Mapping](/docs/current/how-tos/accounting-integrations/how-to-determine-accounts-mapping-for-as-erp-processing)
* [Data Mapping](/docs/current/how-tos/accounting-integrations/how-to-apply-data-mapping-for-as-erp-processing)
* You're familiar with the [Integration Design for accounting periods](/docs/current/integration-design/exports/integration-design-exports-accounting-periods)
## Relevant Export Item Fields
```json theme={null}
{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"accountingEntryId": "59540ed2-0d68-4e36-9e31-58223975d9e9",
"type": "card_purchase",
"subType": null,
"date": "2025-12-10T15:46:34Z",
"amount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"note": "Printer ink",
"files": [
{
"url": "",
"type": "image/jpeg",
"size": 13010
}
],
"supplier": {
"code": "1340472473",
"name": "Target",
"categoryCode": "1000",
"country": "GB",
"account": null,
"taxIdentifier": null
},
"user": {
"id": "6b71f6bd-e83d-4d49-88ee-2b8cda2d57cf",
"name": "Luke Richardson",
"code": null
},
"team": {
"id": "747aaf60-56c6-4b46-ad92-f8a0cb59cf8b",
"code": "5678",
"name": "Engineering"
},
"accountingEntryLines": [
{
"accountingEntryLineId": "0c76ea71-aaaa-4ece-bb68-e1166ccaea04",
"lineAmount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"netAmount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"account": {
"id": "7966c3ba-e4de-4574-8604-6cfa48d62cc8",
"code": "6990000",
"name": "Printing & Stationary",
"identifier": "6990000"
},
"tax": {
"id": "997d8526-5872-484d-ba07-c7a07e08e555",
"code": "0001",
"type": "inclusive",
"amount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 0
},
"inWalletCurrency": {
"currency": "GBP",
"value": 0
}
},
"rate": 0.00
},
"tags": []
}
],
"additionalInformation": {
"reconciliationId": "2500001",
"reconciledEntries": null,
"attendees": [],
"invoiceInformation": null
},
"bookkeeping": {
"method": "journal"
},
"vendor": {
"id": "22e1f2c9-1360-4291-ab41-6b23dcea8888",
"name": "TestVendor",
"code": "acc1234",
"externalId": "ext12345",
"registrationNumber": "reg001234",
"taxRegistrationNumber": "taxreg1234",
"country": "UK",
"defaultCurrency": "GBP"
},
"contraAccount": {
"id": "993d664c-9b7c-4efc-a677-510e69200857",
"code": "0876000",
"name": "0876000_ChartAccounts",
"identifier": "0876000"
},
"_links": {
"web": {
"exportItem": "https://app.staging.pleo.io/export/export-item/0c76ea71-aaaa-4ece-bb68-e1166ccaea04"
}
},
"servicePeriod": null
},
# [other Export Items omitted for brevity]
# [Pagination omitted for brevity]
```
### Expense Date (source date)
This is the transaction date provided by Pleo and represents when the expense occurred.
```json theme={null}
"date": "2023-11-07T05:31:56Z"
```
### Service Period (Optional)
Used for accrual or cost allocation purposes. It does not affect posting date or closed-period handling.
```json theme={null}
"servicePeriod": {
"from": "...",
"to": "..."
}
```
## Steps
### 1. Determine Posting Date
The Export Item provides two dates:
* **Expense date** (`exportItem.date`) — the transaction date from Pleo, representing when the expense occurred
* **Posting date** — the date your integration sends to the Accounting System when creating the entry
By default these are the same:
```pseudo theme={null}
postingDate = exportItem.date
```
The posting date may be adjusted later if the target period is closed (see Steps 3–4).
### 2. Attempt Posting
Submit the accounting entry to the Accounting System using the posting date:
```pseudo theme={null}
postToAccountingSystem(exportItem, postingDate)
```
The Accounting System uses the posting date to determine which period the entry belongs to.
* If the posting **succeeds** — the period is open and the entry is accepted. Proceed to Step 5.
* If the posting is **rejected** due to a closed period — proceed to Step 3.
### 3. Handle Closed Period Rejection
A rejection at Step 2 signals that the posting date falls within a period the Accounting System has closed. This is the trigger to apply a closed period strategy.
```pseudo theme={null}
if postingRejectedDueToClosedPeriod:
applyClosedPeriodStrategy()
```
Closed period handling is not provided by the Export API. It must be implemented as an integration configuration. Whether adjustment is permitted depends on how the integration is configured.
### 4. Apply Closed Period Strategy
The integration must define how to respond when a period is closed. Two options are supported:
#### Option A — Adjust posting date and retry
Adjust the posting date to an open period and return to Step 2:
```pseudo theme={null}
postingDate = exportDate # e.g. current date
```
or
```pseudo theme={null}
postingDate = accountingSystem.findNextOpenPeriod(postingDate)
```
Then retry posting (return to Step 2).
#### Option B — Fail the Export Item
If adjustment is disabled or not permitted by the integration configuration:
```pseudo theme={null}
markExportItemFailed("Closed accounting period")
```
The chosen strategy depends on integration configuration and Accounting System constraints.
### 5. Resolve Accounting Period
Once posting succeeds (either directly at Step 2 or after a date adjustment at Step 4), confirm which accounting period the entry has been assigned to using the final posting date:
```pseudo theme={null}
period = accountingSystem.resolvePeriod(postingDate)
```
This is a confirmation step — the Accounting System resolves the period based on the final, possibly adjusted, posting date. If the Accounting System uses non-calendar or custom fiscal periods, use its APIs or configuration to resolve the correct period rather than assuming a standard January–December structure.
This may involve:
* calendar periods (monthly, quarterly)
* fiscal periods
* custom accounting calendars
## Result
After completing this step:
* A valid posting date has been determined
* Closed period behaviour has been explicitly handled
* The Accounting System assigns the correct accounting period
***
## What Comes Next?
How to Apply Posting Behaviour
***
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# How to Connect Pleo to a Custom API Integration
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-connect-pleo-to-a-custom-api-integration
This how-to explains how to connect Pleo to a Custom API Integration so that export jobs are routed to your integration.
## Prerequisites
Before you begin:
* You have access to the Pleo Web App
* You are logged in as an **Admin** or **Bookkeeper**
## Steps
### 1. Open Accounting Settings
Go to **Settings** and select **Accounting**.
### 2. Disconnect any existing integration
If you have an existing accounting integration connected, disconnect it before proceeding. You will see an option to disconnect on the current integration's settings.
### 3. Search for Custom API Integration
In the search box, search for **Custom API Integration** and select it from the results.
## Result
Pleo is now connected to your custom integration and will route exports accordingly.
***
## What Comes Next?
How to Queue Export Items in Pleo's Web App
***
## Related Reading
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
# How to Detect and Start Export Jobs for Processing
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-detect-and-start-export-jobs-for-as-erp-processing
This how-to explains how an integration detects available Export Jobs in Pleo and starts a job for processing.
Detecting and starting an Export Job is the first step in the export workflow.
Export Jobs group expenses that are ready to be exported to an Accounting System.
At this stage, an Export Job exists with status `pending` and has not yet been started by any integration.
Your integration must:
* Detect available Export Jobs
* Select the appropriate job to process
* Start the job by marking it as `started`
This ensures export jobs are processed in a controlled and predictable way.
## Prerequisites
Before you begin:
* You're familiar with the [Export Lifecycle](/docs/current/platform/exports/lifecycle) and the [Integration Design for Detecting and Starting Export Jobs](/docs/current/integration-design/exports/integration-design-exports-detect-and-start-export-jobs)
* Your integration is authenticated using one of the [supported authentication methods](/docs/current/integration-design/auth/integration-design-auth-overview#authentication-policy-overview)
* Your integration can call Pleo's Export API endpoints
* Webhooks or polling are configured
* [Export Items have been queued in Pleo's Web App](/docs/current/how-tos/accounting-integrations/how-to-queue-export-items-in-ui)
## Steps
### 1. Detect Available Export Jobs
Detect new Export Jobs as they become available.
Three discovery strategies are supported:
#### Option A — Webhook Trigger (Recommended)
Subscribe to the [`export-job.created`](/reference/webhooks/webhook-events) webhook.
When the `export-job.created` event is received, proceed to step 2.
#### Option B — Polling
If webhooks are not supported, periodically request Export Jobs.
Polling should run on a controlled schedule (for example, every few minutes). See step 2 for an example request.
#### Option C — Ad Hoc Trigger (Optional)
If your integration supports user-initiated processing, expose a manual trigger that immediately runs the same polling logic as Option B.
This option is used **alongside** scheduled polling — not instead of it. It allows users to check for pending Export Jobs on demand rather than waiting for the next scheduled interval.
When the trigger fires, proceed to step 2.
***
### 2. Confirm Eligible Job Status
**API Endpoint**: GET [/v3/export-jobs](/reference/export-api/export-api-v3/get-a-list-of-export-jobs)
**Example parameters:** companyId: `12abc3d4-e567-890e-1234-abc56e78fabc`
Request `pending` Export Jobs from the API. If recovering from an interruption, also include `in_progress` to locate a job previously started but not finished.
**Example Pseudo:**
```pseudo theme={null}
// Normal operation
jobs = fetchExportJobs(statuses=["pending"])
// On startup or after reconnection: also check for an interrupted job
if isRecoveryStart:
jobs = fetchExportJobs(statuses=["pending", "in_progress"])
if jobs is empty:
exit workflow
```
#### Example Request — Normal Operation
```bash theme={null}
curl -X GET "https://external.staging.pleo.io/v3/export-jobs?company_id=12abc3d4-e567-890e-1234-abc56e78fabc&statuses=pending" \
-H "Authorization: Bearer "
```
```bash theme={null}
curl --request GET \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
"https://external.staging.pleo.io/v3/export-jobs?company_id=12abc3d4-e567-890e-1234-abc56e78fabc&statuses=pending" \
| jq
```
#### Example Request — Recovery
```bash theme={null}
curl -X GET "https://external.staging.pleo.io/v3/export-jobs?company_id=12abc3d4-e567-890e-1234-abc56e78fabc&statuses=pending&statuses=in_progress" \
-H "Authorization: Bearer "
```
```bash theme={null}
curl --request GET \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
"https://external.staging.pleo.io/v3/export-jobs?company_id=12abc3d4-e567-890e-1234-abc56e78fabc&statuses=pending&statuses=in_progress" \
| jq
```
#### Example Response
```json theme={null}
{
"data": [
{
"id": "8eb648ab-464b-42a0-ba17-eda703657e33",
"createdBy": "f1b5d950-1dbd-4493-8e8c-59fcfe13964f",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"numberOfItems": 4,
"status": "pending",
"expiresIn": 3600,
"createdAt": "2026-04-13T14:36:50Z",
"startedAt": null,
"lastUpdatedAt": "2026-04-13T14:36:50Z",
"completedAt": null,
"expiredAt": null,
"failureReasonType": null,
"failureReason": null,
"isInteractive": true,
"vendorBasedBookkeeping": false
}
],
"pagination": {
"hasPreviousPage": false,
"hasNextPage": false,
"currentRequestPagination": {
"sortingKeys": [],
"sortingOrder": [],
"parameters": {
"company_id": [
"12abc3d4-e567-890e-1234-abc56e78fabc"
],
"statuses": [
"pending"
]
}
},
"startCursor": "AAAAAADJ3T7YEMLAE3UA=R23ERK2GJNBKBOQX5WTQGZL6GM",
"endCursor": "AAAAAADJ3T7YEMLAE3UA=R23ERK2GJNBKBOQX5WTQGZL6GM",
"total": 1
}
}
```
***
### 3. Select the Oldest Export Job
From the filtered results, always select the **oldest** eligible job. Never process jobs in parallel. Maintain a single export worker to guarantee sequential execution.
**Example Pseudo:**
```pseudo theme={null}
jobToProcess = sortByCreatedAt(eligibleJobs).first()
```
***
### 4. Verify the Job Status
**API Endpoint**: GET [`/v3/export-jobs/{jobId}`](/reference/export-api/export-api-v3/get-an-export-job)
**Example parameters:** jobId: `8eb648ab-464b-42a0-ba17-eda703657e33`
Fetch the individual job to confirm its current status before proceeding. The status determines which path to take next.
**Example:**
```pseudo theme={null}
if job.status == "pending":
// New job — proceed to Step 5 to start it
continue to start job
elif job.status == "in_progress":
// Recovery — this job was previously started by this integration
// Skip Step 5 and proceed directly to pre-export validation
continue to pre-export validation
else:
// Job is completed, failed, or expired — nothing to do
skip job
```
#### Example Request
```bash theme={null}
curl -X GET "https://external.staging.pleo.io/v3/export-jobs/8eb648ab-464b-42a0-ba17-eda703657e33" \
-H "Authorization: Bearer "
```
```json theme={null}
curl --request GET \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
"https://external.staging.pleo.io/v3/export-jobs/8eb648ab-464b-42a0-ba17-eda703657e33" \
| jq
```
#### Example Response
```json theme={null}
{
"data": {
"id": "8eb648ab-464b-42a0-ba17-eda703657e33",
"createdBy": "f1b5d950-1dbd-4493-8e8c-59fcfe13964f",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"numberOfItems": 4,
"status": "pending",
"expiresIn": 3600,
"createdAt": "2026-04-13T14:36:50Z",
"startedAt": null,
"lastUpdatedAt": "2026-04-13T14:36:50Z",
"completedAt": null,
"expiredAt": null,
"failureReasonType": null,
"failureReason": null,
"isInteractive": true,
"vendorBasedBookkeeping": false
}
}
```
***
### 5. Start the Export Job
**API Endpoint**: POST [`/v3/export-job-events`](/reference/export-api/export-api-v3/create-an-export-job-event)
**Only applies if `job.status == "pending"` (from Step 4).** If the job is already `in_progress`, skip this step and proceed directly to pre-export validation.
Mark the Export Job event as `started` to signal to Pleo that your integration has taken responsibility for processing.
Marking an Export Job Event as started via `/v3/export-job-events` (`"event": "started"`) updates the Export Job status to `"in_progress"`, which can be retrieved from `/v3/export-jobs/{jobId}`.
**Example:**
```pseudo theme={null}
markExportJobStarted(job.id)
```
#### Example Request
```bash theme={null}
curl -X POST "https://external.staging.pleo.io/v3/export-job-events" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json;charset=UTF-8" \
-d '{
"event": "started",
"jobId": "8eb648ab-464b-42a0-ba17-eda703657e33"
}'
```
```json theme={null}
curl --request POST \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v3/export-job-events" \
-d '{
"event": "started",
"jobId": "8eb648ab-464b-42a0-ba17-eda703657e33"
}'
```
#### Example Response
If the request succeeds, rerunning the Step 4 curl command will return an updated job status of `in_progress`.
```json theme={null}
{
"status": "in_progress"
}
```
***
### 6. Handle Concurrent Start Conflicts Safely
Integrations are designed to run a single export worker — but infrastructure doesn't always guarantee this. Rolling deployments, double-firing scheduled jobs, or a restarting worker can briefly produce two instances that both attempt to start the same job at the same moment.
If the API returns a `422 INVALID_EXPORT_JOB_STATUS_CHANGE` response, the job has already been started by another instance.
* Treat the response as expected behaviour
* Do not retry aggressively
* Fetch jobs again and continue normally
**Example:**
```pseudocode theme={null}
try:
markJobStarted(job)
catch ConflictError:
log("Job already started")
restart discovery cycle
```
### 7. Process the Job in a Timely Manner
Export jobs expire after `expiresIn` seconds (e.g., 3600) since the last state-changing update (`lastUpdatedAt`).
To avoid expiration:
* Begin processing immediately after starting the job
* Ensure progress is made before the expiry window elapses
* Update the job status as work progresses to refresh `lastUpdatedAt` (e.g., move from `pending` to `in_progress`)
If a job expires:
* The status moves to `expired`
* The job must be resubmitted from Pleo's Web App
* A new job with a new `jobId` is added to the queue
Integrations should ensure their processing is **idempotent**, as items from an expired job may be included again in a newly submitted job.
***
## Result
After completing these steps:
* An Export Job has been safely detected
* The oldest eligible job has been selected
* Your integration has taken responsibility for processing
* The export workflow can proceed
***
## What Comes Next?
How to Perform Pre-Export Validation
***
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# How to Apply Accounts Mapping for Export Items
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-determine-accounts-mapping-for-as-erp-processing
Accounts Mapping determines which **ledger (GL) accounts** are used when creating accounting entries.
This step converts Export Item data into **debit and credit postings** in your Accounting System.
## Prerequisites
Before you begin:
* You've fetched the [Export Item Payload Data](/docs/current/how-tos/accounting-integrations/how-to-fetch-export-item-data-for-as-erp-processing)
* You've determined the [Bookkeeping Method](/docs/current/how-tos/accounting-integrations/how-to-determine-the-bookkeeping-method-for-as-erp-processing)
* You're familiar with the [Integration Design for Determining the Accounts Mapping](/docs/current/integration-design/exports/integration-design-exports-accounts-mapping)
## Relevant Export Item Fields
```json theme={null}
{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"accountingEntryId": "59540ed2-0d68-4e36-9e31-58223975d9e9",
"type": "card_purchase",
"subType": null,
"date": "2025-12-10T15:46:34Z",
"amount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"note": "Printer ink",
"files": [
{
"url": "",
"type": "image/jpeg",
"size": 13010
}
],
"supplier": {
"code": "1340472473",
"name": "Target",
"categoryCode": "1000",
"country": "GB",
"account": null,
"taxIdentifier": null
},
"user": {
"id": "6b71f6bd-e83d-4d49-88ee-2b8cda2d57cf",
"name": "Luke Richardson",
"code": null
},
"team": {
"id": "747aaf60-56c6-4b46-ad92-f8a0cb59cf8b",
"code": "5678",
"name": "Engineering"
},
"accountingEntryLines": [
{
"accountingEntryLineId": "0c76ea71-aaaa-4ece-bb68-e1166ccaea04",
"lineAmount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"netAmount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"account": {
"id": "7966c3ba-e4de-4574-8604-6cfa48d62cc8",
"code": "6990000",
"name": "Printing & Stationary",
"identifier": "6990000"
},
"tax": {
"id": "997d8526-5872-484d-ba07-c7a07e08e555",
"code": "0001",
"type": "inclusive",
"amount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 0
},
"inWalletCurrency": {
"currency": "GBP",
"value": 0
}
},
"rate": 0.00
},
"tags": []
}
],
"additionalInformation": {
"reconciliationId": "2500001",
"reconciledEntries": null,
"attendees": [],
"invoiceInformation": null
},
"bookkeeping": {
"method": "journal"
},
"vendor": {
"id": "22e1f2c9-1360-4291-ab41-6b23dcea8888",
"name": "TestVendor",
"code": "acc1234",
"externalId": "ext12345",
"registrationNumber": "reg001234",
"taxRegistrationNumber": "taxreg1234",
"country": "UK",
"defaultCurrency": "GBP"
},
"contraAccount": {
"id": "993d664c-9b7c-4efc-a677-510e69200857",
"code": "0876000",
"name": "0876000_ChartAccounts",
"identifier": "0876000"
},
"_links": {
"web": {
"exportItem": "https://app.staging.pleo.io/export/export-item/0c76ea71-aaaa-4ece-bb68-e1166ccaea04"
}
},
"servicePeriod": null
},
# [other Export Items omitted for brevity]
# [Pagination omitted for brevity]
```
### Expense Account (line-level)
```json theme={null}
"accountingEntryLines": [
{
"account": {
"id": "...",
"code": "",
"name": "Meals and Entertainment"
}
}
]
```
### Contra Account
```json theme={null}
"contraAccount": {
"id": "...",
"code": "",
"name": "Pleo Wallet Account"
}
```
### Bookkeeping Method
```json theme={null}
"bookkeeping": {
"method": "journal"
}
```
### Vendor
```json theme={null}
"vendor": {
"code": "",
"name": "ACME Ltd"
}
```
## Steps
### 1. Determine Mapping Strategy
Mapping depends on:
* `bookkeeping.method`
* Export Item `type / subType`
* availability of `vendor`
* configured mapping rules
Accounts mapping is not static — it depends on bookkeeping method and expense type.
### 2. Apply Expense Account Mapping
For all cases:
* The **expense (debit) side** comes from:
```pseudo theme={null}
expenseAccount = mappingTable.get(accountingEntryLines[].account.code)
```
Each line is mapped individually.
### 3. Select Counter Account
### Case A: Journal Entries
If:
```json theme={null}
"bookkeeping.method": "journal"
```
Then:
* Counter account typically comes from:
```pseudo theme={null}
contraAccount = mappingTable.get(contraAccount.code)
```
Used for:
* wallet
* bank
* clearing accounts
### Case B: Accounts Payable
If:
```json theme={null}
"bookkeeping.method": "accounts_payable"
```
Then:
1. Attempt vendor match:
```pseudo theme={null}
vendorAccount = resolveVendorAccount(vendor.code)
```
2. Apply fallback:
```pseudo theme={null}
if vendorAccount exists:
counterAccount = vendorAccount
else:
counterAccount = defaultAccountsPayableAccount
```
Vendor-specific Accounts Payable accounts override default AP accounts when available.
### 4. Apply Expense Type Rules (if applicable)
Depending on Export Item type:
* `card_purchase` → contra account (wallet/bank)
* `invoice` → AP account
* `wallet_topup` → configured mapping accounts
Ensure your mapping logic supports these variations.
### 5. Validate Mapping
Before posting:
* Account exists in Accounting System
* Mapping is configured
* Currency is compatible
If validation fails:
* mark Export Item as failed
* return clear failure reason
## Results
After completing this step:
* Each Export Item has:
* a mapped **expense (debit) account**
* a mapped **counter (credit) account**
* Entries are **balanced and ready for posting**
***
## What Comes Next?
How to Apply Data Mapping
***
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# How to Determine the Bookkeeping Method
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-determine-the-bookkeeping-method-for-as-erp-processing
This how-to explains how to determine which downstream accounting workflow should be used for each Export Item based on the `bookkeeping.method` field provided by Pleo.
## Prerequisites
Before you begin:
* You're familiar with the [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* You're familiar with the [Integration Design for Determining the Bookkeeping Method](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-resolution)
* You've [fetched the export item payload data](/docs/current/how-tos/accounting-integrations/how-to-fetch-export-item-data-for-as-erp-processing)
```json theme={null}
{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"accountingEntryId": "59540ed2-0d68-4e36-9e31-58223975d9e9",
"type": "card_purchase",
"subType": null,
"date": "2025-12-10T15:46:34Z",
"amount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"note": "Printer ink",
"files": [
{
"url": "",
"type": "image/jpeg",
"size": 13010
}
],
"supplier": {
"code": "1340472473",
"name": "Target",
"categoryCode": "1000",
"country": "GB",
"account": null,
"taxIdentifier": null
},
"user": {
"id": "6b71f6bd-e83d-4d49-88ee-2b8cda2d57cf",
"name": "Luke Richardson",
"code": null
},
"team": {
"id": "747aaf60-56c6-4b46-ad92-f8a0cb59cf8b",
"code": "5678",
"name": "Engineering"
},
"accountingEntryLines": [
{
"accountingEntryLineId": "0c76ea71-aaaa-4ece-bb68-e1166ccaea04",
"lineAmount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"netAmount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"account": {
"id": "7966c3ba-e4de-4574-8604-6cfa48d62cc8",
"code": "6990000",
"name": "Printing & Stationary",
"identifier": "6990000"
},
"tax": {
"id": "997d8526-5872-484d-ba07-c7a07e08e555",
"code": "0001",
"type": "inclusive",
"amount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 0
},
"inWalletCurrency": {
"currency": "GBP",
"value": 0
}
},
"rate": 0.00
},
"tags": []
}
],
"additionalInformation": {
"reconciliationId": "2500001",
"reconciledEntries": null,
"attendees": [],
"invoiceInformation": null
},
"bookkeeping": {
"method": "journal"
},
"vendor": {
"id": "22e1f2c9-1360-4291-ab41-6b23dcea8888",
"name": "TestVendor",
"code": "acc1234",
"externalId": "ext12345",
"registrationNumber": "reg001234",
"taxRegistrationNumber": "taxreg1234",
"country": "UK",
"defaultCurrency": "GBP"
},
"contraAccount": {
"id": "993d664c-9b7c-4efc-a677-510e69200857",
"code": "0876000",
"name": "0876000_ChartAccounts",
"identifier": "0876000"
},
"_links": {
"web": {
"exportItem": "https://app.staging.pleo.io/export/export-item/0c76ea71-aaaa-4ece-bb68-e1166ccaea04"
}
},
"servicePeriod": null
},
# [other Export Items omitted for brevity]
# [Pagination omitted for brevity]
```
## Steps
### 1. Read the Bookkeeping Method
The bookkeeping method is provided directly in the Export Item:
```json theme={null}
{
"bookkeeping": {
"method": "journal"
}
}
```
### 2. Determine the Workflow
Map the bookkeeping method to the correct processing workflow:
| Method | Workflow |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `journal` | Journal Entry Handling |
| `accounts_payable` | Accounts Payable Handling |
| `none` | Default to Journal Entry workflow |
| `null` | [Legacy Support](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-accounts-payable#legacy-support) |
### 3. Route the Export Item
Based on the resolved method:
* If `journal` → process using the [Journal Entry Handling](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-journal-entry) workflow
* If `accounts_payable` → process using the [Accounts Payable Handling](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-accounts-payable) workflow
* If `none` → treat as `journal`
### Example Pseudo
```pseudo theme={null}
method = item.bookkeeping.method
if method == "journal":
processAsJournalEntry(item)
elif method == "accounts_payable":
processAsAccountsPayable(item)
elif method == "none":
processAsJournalEntry(item)
else:
failItem(item, "unsupported_bookkeeping_method")
```
This step only determines routing. It does not execute any accounting logic.
## Result
After completing these steps, each Export Item is routed into the correct downstream accounting processing workflow.
***
## What Comes Next?
How to Apply Accounts Mapping
***
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [Journal Entry Handling](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-journal-entry)
* [Accounts Payable Handling](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-accounts-payable)
***
# How to Enable Vendor-Based Bookkeeping
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-enable-vendor-based-bookkeeping
This article explains why the `bookkeeping` field returns `null` in the `/v3/export-items` response and how to enable vendor-based bookkeeping so it returns a value.
## Why bookkeeping Returns null
The `bookkeeping` field in the export items response is only populated when the **Vendor Tagging** feature is enabled and a bookkeeping method has been assigned to the expense. Without Vendor Tagging enabled, `bookkeeping` will always be `null`. This is expected behaviour, not an error.
```json theme={null}
{
"bookkeeping": null
}
```
## What to Do When bookkeeping Is null
When `bookkeeping` is `null`, use the `supplier` field on the export item for bookkeeping purposes instead. This is supported for all integrations.
See [Bookkeeping Method Resolution](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-resolution) for how to handle the `null` case in your integration logic.
## Enable Vendor-Based Bookkeeping
### 1. Activate Vendor Tagging
1. Click **Settings** from the main left-hand menu
2. Click **Accounting**
3. Select **Activate Vendor Tagging** at the bottom of your screen
### Vendor Tagging option not visible?
If you do not see the **Activate Vendor Tagging** option, first make sure you have at least one vendor created via the API.
**API Endpoint**: POST [`/v1/vendors`](/reference/external-vendors/create-a-new-vendor)
**Example parameters**: companyId: 12abc3d4-e567-890e-1234-abc56e78fabc
```bash theme={null}
curl --request POST \
--url https://external.staging.pleo.io/v1/vendors \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json;charset=UTF-8' \
--data '
{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"defaultCurrency": "GBP",
"name": "TestVendor",
"code": "1234",
"country": "UK",
"externalId": "ext-1234",
"registrationNumber": "Regs-123456",
"taxRegistrationNumber": "Regstax-12345678"
}'
```
```bash theme={null}
curl --request POST \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v1/vendors" \
-d'
{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"defaultCurrency": "GBP",
"name": "TestVendor2",
"code": "1234",
"country": "UK",
"externalId": "ext-1234",
"registrationNumber": "Regs-123456",
"taxRegistrationNumber": "Regstax-12345678"
}'
```
If it is still not visible after creating a vendor, [contact Pleo Support](https://help.pleo.io/en/support/home) to
have it enabled for your account.
### 2. Select Vendor
1. Click **Settings** from the main left-hand menu
2. Click **Accounting**
3. Select the **Vendor** you created via the API from the **Vendor Tagging** drop-down menu
### 3. Assign bookkeeping methods to expenses
Once Vendor Tagging is active, each expense has a default bookkeeping method
assigned automatically:
* **Journal** is the default for most expenses
* **Accounts Payable** is the default for invoices
Before adding an expense to the export queue, bookkeepers can change the
assigned method if needed. This gives them the flexibility to override the
default where their accounting requirements differ.
### 4. Queue expenses and call the API
After expenses have been queued, the `bookkeeping.method` field in the
`/v3/export-items` response will return one of the following values:
| Value | Meaning |
| ------------------ | ---------------------------------------------------------------------------------- |
| `journal` | Record as a journal entry |
| `accounts_payable` | Record via the accounts payable flow |
| `none` | Record as a journal entry (used for wallet loads, unloads, and balance amendments) |
***
## Related Reading
* [Bookkeeping Method Resolution](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-resolution)
* [How to Determine the Bookkeeping Method](/docs/current/how-tos/accounting-integrations/how-to-determine-the-bookkeeping-method-for-as-erp-processing)
* [Fetch Export Item Data](/docs/current/integration-design/exports/integration-design-exports-fetch-export-items-data-layer)
***
# How to Fetch Export Item Data for Processing
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-fetch-export-item-data-for-as-erp-processing
Fetching Export Item Data is the step where integrations retrieve the full accounting payload required for processing.
This step operates on the items identified in the [How to Retrieve Export Job Items for Processing](/docs/current/how-tos/accounting-integrations/how-to-retrieve-export-job-items-for-as-erp-processing) article.
This endpoint returns **processing data**, not control state.
## Prerequisites
Before you begin:
* You've completed [How to Retrieve Export Job Items for Processing](/docs/current/how-tos/accounting-integrations/how-to-retrieve-export-job-items-for-as-erp-processing)
* You have a `jobId`
* You have identified items to process (e.g. `pending`)
## Steps (Data Layer)
### 1. Fetch Full Export Item Data
**API Endpoint**: GET [/v3/export-items](/reference/export-api/export-items-v3/get-export-items)
**Example parameters:** jobId: `8eb648ab-464b-42a0-ba17-eda703657e33`
Once Export Items have been identified for the job, retrieve the full accounting payload for processing.
The `export-items` endpoint returns all Export Items for the job, including:
* transaction details
* accountingEntryLines
* supplier/vendor data
* bookkeeping metadata
* attachments
Each item must be processed individually using this payload.
**Example Pseudo:**
```pseudo theme={null}
items = fetchFullExportItems(jobId)
for item in items:
process(item)
```
#### Example Request
```bash theme={null}
curl -X GET "https://external.staging.pleo.io/v3/export-items?job_id=8eb648ab-464b-42a0-ba17-eda703657e33" \
-H "Authorization: Bearer "
```
```bash theme={null}
curl --request GET \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
"https://external.staging.pleo.io/v3/export-items?job_id=8eb648ab-464b-42a0-ba17-eda703657e33" \
| jq
```
#### Example Response
```json theme={null}
{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"accountingEntryId": "59540ed2-0d68-4e36-9e31-58223975d9e9",
"type": "card_purchase",
"subType": null,
"date": "2025-12-10T15:46:34Z",
"amount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"note": "Printer ink",
"files": [
{
"url": "",
"type": "image/jpeg",
"size": 13010
}
],
"supplier": {
"code": "1340472473",
"name": "Target",
"categoryCode": "1000",
"country": "GB",
"account": null,
"taxIdentifier": null
},
"user": {
"id": "6b71f6bd-e83d-4d49-88ee-2b8cda2d57cf",
"name": "Luke Richardson",
"code": null
},
"team": {
"id": "747aaf60-56c6-4b46-ad92-f8a0cb59cf8b",
"code": "5678",
"name": "Engineering"
},
"accountingEntryLines": [
{
"accountingEntryLineId": "0c76ea71-aaaa-4ece-bb68-e1166ccaea04",
"lineAmount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"netAmount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"account": {
"id": "7966c3ba-e4de-4574-8604-6cfa48d62cc8",
"code": "6990000",
"name": "Printing & Stationery",
"identifier": "6990000"
},
"tax": {
"id": "997d8526-5872-484d-ba07-c7a07e08e555",
"code": "0001",
"type": "inclusive",
"amount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 0
},
"inWalletCurrency": {
"currency": "GBP",
"value": 0
}
},
"rate": 0.00
},
"tags": []
}
],
"additionalInformation": {
"reconciliationId": "2500001",
"reconciledEntries": null,
"attendees": [],
"invoiceInformation": null
},
"bookkeeping": {
"method": "journal"
},
"vendor": {
"id": "22e1f2c9-1360-4291-ab41-6b23dcea8888",
"name": "TestVendor",
"code": "acc1234",
"externalId": "ext12345",
"registrationNumber": "reg001234",
"taxRegistrationNumber": "taxreg1234",
"country": "UK",
"defaultCurrency": "GBP"
},
"contraAccount": {
"id": "993d664c-9b7c-4efc-a677-510e69200857",
"code": "0876000",
"name": "0876000_ChartAccounts",
"identifier": "0876000"
},
"_links": {
"web": {
"exportItem": "https://app.staging.pleo.io/export/export-item/0c76ea71-aaaa-4ece-bb68-e1166ccaea04"
}
},
"servicePeriod": null
},
# [other Export Items omitted for brevity]
# [Pagination omitted for brevity]
```
### 2. Handle Pagination
Export Items may be returned across multiple pages.
Always retrieve all pages before beginning processing to avoid partial exports.
#### Example Response
```json theme={null}
"pagination": {
"hasNextPage": true,
"endCursor": ""
}
```
**Example Pseudo:**
```pseudo theme={null}
items = []
do:
response = fetchItems(cursor)
items.append(response.data)
cursor = response.pagination.endCursor
while response.pagination.hasNextPage
```
### 3. Validate Each Export Item
Before attempting to record an item in your Accounting System, validate that it has everything required to be processed. This check runs **per item**. A failure on one item does not block others.
Two checks are required:
**Check 1: Required configurations are present**
Verify that all integration configurations needed to export this specific item exist (e.g. the journal is configured,
required accounts are mapped).
**Check 2: Expense GL Account (Category) is present**
Verify that the item has an Expense GL Account assigned via `accountingEntryLines[].account`.
If either check fails, **do not attempt to record the item in the AS**. Instead, mark it as failed immediately and
move to the next item.
```pseudo theme={null}
for item in items:
if not hasRequiredConfigurations(item):
failItem(item,
failureReasonType = "missing_configuration",
failureReason = "[Account/Journal X] missing: provide [X] in the configurations"
)
continue
if not hasExpenseGLAccount(item):
failItem(item,
failureReasonType = "missing_configuration",
failureReason = "Account [X] missing: provide account [X] in the configurations"
)
continue
process(item)
```
Failed items must still be reported back to Pleo. See [How to Update Export\
Items](/docs/current/how-tos/accounting-integrations/how-to-update-export-items-for-as-erp-processing) for how to set
`status: failed` with the appropriate `failureReasonType` and `failureReason`.
### 4. Handle Job Recovery
If your integration restarts or loses state, it must be able to resume processing safely.
See: [How to Retrieve Export Job Items for Processing](/docs/current/how-tos/accounting-integrations/how-to-retrieve-export-job-items-for-as-erp-processing#3-handle-job-recovery-in_progress-jobs)
## Result
After completing these steps:
* All Export Items with full payloads for the job have been retrieved
* Pagination has been fully resolved
* The integration is ready to process items sequentially
* The workflow remains resumable and consistent
* Each item has been validated for required configurations and GL Account before
AS processing begins
***
## What Comes Next?
Up to this point, you’ve completed steps 1–4 of the [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide).
Step 5 moves processing **from** Pleo’s export workflow **to** your Accounting System or ERP, where Export Items are processed and recorded, as outlined in the [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide).
Once processing is complete, return to steps 6 and 7 of the [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide) to update the status of each Export Item and finalise the Export Job.
AS/ERP Processing Workflow Guide: Step 1: How to determine bookkeeping method
***
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
## FAQs
You're getting a `MISSING_CONTRA_ACCOUNTS` error, because the Export API v3 requires a contra account to be configured for each entity's default currency. If any entity's default currency does not have a contra account mapped, calls to `/v3/export-items` will fail with this error.
See [How to Resolve MISSING\_CONTRA\_ACCOUNTS](/docs/current/how-tos/accounting-integrations/how-to-resolve-missing-contra-accounts) for steps to fix this.
You're getting a `null` value for the `bookkeeping` field, because it's only populated when the **Vendor Tagging** feature is enabled and a bookkeeping method has been assigned to the expense before it was queued. Without Vendor Tagging enabled, `bookkeeping` will always be `null`. This is expected behaviour, not an error.
When `bookkeeping` is `null`, use the `supplier` field on the export item for bookkeeping purposes instead.
See [How to Enable Vendor-Based Bookkeeping](/docs/current/how-tos/accounting-integrations/how-to-enable-vendor-based-bookkeeping) for steps to enable it.
***
# How to Perform Pre-Export Validation
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-perform-pre-export-validation-for-as-erp-processing
This how-to explains how an integration validates readiness before processing Export Items.
Pre-export validation ensures that the Accounting System and integration configuration are ready before any data is transferred.
This step occurs after an Export Job has been started and before any Export Items are fetched or processed.
At this stage, the Export Job is in progress and no accounting entries have been created, so failures can safely stop the workflow.
Your integration must verify:
* The Accounting System is available
* The correct company context is selected for multi-entity scenarios
* The correct Integration Configuration exists
## Prerequisites
Before you begin:
* You're familiar with the [Export Lifecycle](/docs/current/platform/exports/lifecycle) and the [Integration Design for Pre-Export Validation](/docs/current/integration-design/exports/integration-design-exports-pre-export-validation)
* An Export Job has been successfully [Detected and Started](/docs/current/how-tos/accounting-integrations/how-to-detect-and-start-export-jobs-for-as-erp-processing) for processing
* You can call both Pleo APIs and the target Accounting System APIs
## Steps
### 1. Validate Accounting System Connectivity
Ensure the Accounting System is reachable and ready to accept data.
Confirm:
* API authentication is valid
* required permissions are available
* key operations are supported
Typical checks include:
* ability to create journal entries
* support for vendor bookkeeping (if applicable)
* support for attachments
**Example Pseudo:**
```pseudo theme={null}
if not canAuthenticate():
failExportJob(job.id, "accounting_system_unreachable")
if not hasRequiredPermissions():
failExportJob(job.id, "invalid_configuration")
```
***
### 2. Validate Company Context
**API Endpoint**: GET [/v3/export-jobs](/reference/export-api/export-api-v3/get-a-list-of-export-jobs)
**Example parameters:** companyId: `12abc3d4-e567-890e-1234-abc56e78fabc`
Use Export Job metadata to ensure the correct company context is selected. This is important in [multi-entity](/docs/current/platform/multi-entity/multi-entity-and-api-scope) setups.
```json theme={null}
{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc"
}
```
**Example Pseudo:**
```pseudo theme={null}
if not companyExists(job.companyId):
failExportJob(job.id, "invalid_configuration")
```
### 3. Validate Bookkeeping Capability
**API Endpoint**: GET [/v3/export-jobs](/reference/export-api/export-api-v3/get-a-list-of-export-jobs)
Export Jobs may require specific bookkeeping workflows.
For example:
```json theme={null}
{
"vendorBasedBookkeeping": true
}
```
If vendor-based bookkeeping is required, your integration must support it.
**Example Pseudo:**
```pseudo theme={null}
if job.vendorBasedBookkeeping and not supportsVendorMode():
failExportJob("invalid_configuration")
```
Vendor-based bookkeeping (AP-style) means expenses/invoices can be exported against a specific vendor/supplier (vendor ledger) rather than as generic journal entries; when enabled via `vendorBasedBookkeeping`, integrators must be able to create the correct record type based on each item’s [`bookkeeping.method`](/docs/current/how-tos/accounting-integrations/how-to-determine-the-bookkeeping-method-for-as-erp-processing).
### 4. Fail Early if Validation Fails
**API Endpoint**: POST [/v3/export-job-events](/reference/export-api/export-api-v3/create-an-export-job-event)
**Example parameters:** jobId: `8eb648ab-464b-42a0-ba17-eda703657e33`
If any validation step fails, the Export Job must be explicitly marked as failed.
Failing early ensures:
* no Export Items are processed
* the system remains consistent
* users receive clear feedback in Pleo's Web App
**Example Pseudo:**
```pseudo theme={null}
if validationFails:
failExportJob(job.id, reason)
stop processing
```
#### Example Request
```bash theme={null}
curl -X POST "https://external.staging.pleo.io/v3/export-job-events" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"event": "failed",
"jobId": "8eb648ab-464b-42a0-ba17-eda703657e33",
"failureReason": "vendorBasedBookkeeping not supported by this integration",
"failureReasonType": "invalid_configuration"
}'
```
```bash theme={null}
curl --request POST \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v3/export-job-events" \
-d '{
"event": "failed",
"jobId": "8eb648ab-464b-42a0-ba17-eda703657e33",
"failureReason": "vendorBasedBookkeeping not supported by this integration",
"failureReasonType": "invalid_configuration"
}'
```
#### Example Response
**API Endpoint**: GET [`/v3/export-jobs/{jobId}`](/reference/export-api/export-api-v3/get-an-export-job)
If the request succeeds, re-running the `/v3/export-jobs/{jobId}` command will return an updated status of `failed`.
```json theme={null}
{
"status": "failed",
"failureReasonType": "invalid_configuration",
"failureReason": "vendorBasedBookkeeping not supported by this integration"
}
```
#### Pleo Web App
The failed export status is updated in the Pleo Web App. Once any issues have been resolved, rerun the export job by clicking **Export** at the bottom of the screen.
Each time you click **Export**, a new job is created in the export queue with a unique `jobId`.
#### Common Failure Reasons
Use clear and actionable failure reasons:
* `invalid_configuration` — unsupported workflows
* `missing_configuration` — missing mappings or setup
* `accounting_system_unreachable` — connectivity or API issues
Failure reason types should align with the [Export API specification](/reference/export-api/export-api-v3/create-an-export-job-event#body-failure-reason-type).
Fail the Export Job only when the issue prevents **all items** from being processed.
Item-level issues should be handled later during Export Item processing.
## Result
After completing these steps:
* The integration has verified readiness for export processing
* Invalid or unsupported scenarios are safely rejected
* Only valid Export Jobs proceed to item processing
***
## What Comes Next?
Fetch Export Items and begin processing individual expenses
***
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# How to Queue Export Items in Pleo's Web App
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-queue-export-items-in-ui
This how-to explains how to queue expenses in Pleo’s Web App so they can be picked up and processed by an accounting integration.
## Prerequisites
Before you begin:
* You have access to the Pleo Web App
* You are logged in as an **Admin** or **Bookkeeper**
## Steps
### 1. Open Exports
Select **Exports** from the main left-hand navigation.
### 2. Select expenses
Check the tickboxes next to the expenses you want to export.
### 3. Add to Export Queue
Click **Add to Export Queue** at the bottom of your screen.
### 4. Open Export Queue
Click the **Export Queue** tab at the top of your screen.
### 5. Confirm queued expenses
In the **Export Queue** tab, select the expenses you want to include in the export.
### 6. Create Export Job
Click **Export** at the bottom of your screen.
### 7. Export Job is created
Once you click **Export**:
* An **Export Job** is created
* The selected expenses become **Export Items**
* These items are locked and can no longer be edited
Export Items cannot be modified after they are added to an Export Job.
## Result
After completing these steps:
* An **Export Job** has been created
* The selected expenses are now **Export Items**
* These items are available for your integration to detect and process
***
## What Comes Next?
How to Detect and Start Export Jobs for Processing
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# How to Resolve MISSING_CONTRA_ACCOUNTS
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-resolve-missing-contra-accounts
This article explains why the `MISSING_CONTRA_ACCOUNTS` error occurs when calling `/v3/export-items` and how to resolve it.
## The Error
When calling `GET /v3/export-items`, you may receive the following response:
```json theme={null}
{
"type": "MISSING_CONTRA_ACCOUNTS",
"message": "Make sure you don't export GBP expenses before you set your contra accounts."
}
```
## Why This Happens
Export API v3 requires a contra account to be configured for each entity's default currency. The default currency is determined by the region where the company entity is registered, for example, GBP for a UK entity or
DKK for a Danish entity.
In a multi-entity setup, each entity has its own default currency, and a contra account must be configured for each one. If any entity's default currency does not have a contra account mapped, calls to `/v3/export-items` will fail with
this error.
## How to Resolve It
### Add a Contra Account
1. Click **Settings** from the main left-hand menu
2. Click **Accounting**
3. Click the **Contra accounts** tab
4. Select a Contra account for your default currency from the drop-down menu
A contra account must be configured for every default currency in use. Expenses in a
currency without a mapped contra account will continue to return this error.
### Contra Accounts tab not visible?
If you do not see the **Contra Accounts** tab, [contact Pleo Support](https://help.pleo.io/en/support/home) to have it enabled for your account.
## After Resolving
Once contra accounts are configured for the default currency, calls to `GET /v3/export-items` will return the full export item payload as expected.
***
## Related Reading
* [Fetch Export Item Data](/docs/current/integration-design/exports/integration-design-exports-fetch-export-items-data-layer)
* [How to Fetch Export Item Data for Processing](/docs/current/how-tos/accounting-integrations/how-to-fetch-export-item-data-for-as-erp-processing)
***
# How to Retrieve Export Job Items for Processing
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-retrieve-export-job-items-for-as-erp-processing
Retrieving Export Job Items is the first step in the export item processing phase.
This step determines which Export Items must be processed, but does not include the accounting data itself.
## Prerequisites
Before you begin:
* You're familiar with the [Export Lifecycle](/docs/current/platform/exports/lifecycle) and the [Integration Design for Fetching Export Job Items](/docs/current/integration-design/exports/integration-design-exports-fetch-export-items-control-layer)
* The [Export Job has been started](/docs/current/how-tos/accounting-integrations/how-to-detect-and-start-export-jobs-for-as-erp-processing) (`"event": "started"`, `"status": "in_progress"`)
* [Pre-export validation has completed](/docs/current/how-tos/accounting-integrations/how-to-perform-pre-export-validation-for-as-erp-processing) and the integration is ready to begin processing data
* The Export `jobId` is available
## Steps (Control Layer)
### 1. Retrieve Export Items for the Job
**API Endpoint**: GET [/v3/export-jobs/`{jobId}`/items](/reference/export-api/export-api-v3/get-export-job-items)
**Example parameters:** jobId: `8eb648ab-464b-42a0-BA17-eda703657e33`
Returns control data:
* accountingEntryId
* status (pending, in\_progress, etc.)
* export tracking fields
This endpoint is used to:
* determine processing scope
* track progress
* recover from interruptions
#### Example Request
```bash theme={null}
curl --request GET \
--url https://external.staging.pleo.io/v3/export-jobs/8eb648ab-464b-42a0-BA17-eda703657e33/items \
--header 'Authorization: Bearer '
```
```bash theme={null}
curl --request GET \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
"https://external.staging.pleo.io/v3/export-jobs/8eb648ab-464b-42a0-BA17-eda703657e33/items" \
| jq
```
#### Example Response
Each Export Item represents a single accounting entry to be processed.
The Export Job is `in_progress`, but each Export Item remains in `pending` until it has been processed by the integration.
```json theme={null}
{
"data": [
{
"exportJobId": "8eb648ab-464b-42a0-ba17-eda703657e33",
"accountingEntryId": "59540ed2-0d68-4e36-9e31-58223975d9e9",
"status": "pending",
"externalId": null,
"externalUrl": null,
"failureReasonType": null,
"failureReason": null,
"exportedAt": null
},
{
"exportJobId": "8eb648ab-464b-42a0-ba17-eda703657e33",
"accountingEntryId": "6678bcee-c6a4-4b49-bb1e-8f9fd653d16c",
"status": "pending",
"externalId": null,
"externalUrl": null,
"failureReasonType": null,
"failureReason": null,
"exportedAt": null
},
{
"exportJobId": "8eb648ab-464b-42a0-ba17-eda703657e33",
"accountingEntryId": "9ea2ef81-4d73-460d-bbb4-ccf3da6f6f61",
"status": "pending",
"externalId": null,
"externalUrl": null,
"failureReasonType": null,
"failureReason": null,
"exportedAt": null
},
{
"exportJobId": "8eb648ab-464b-42a0-ba17-eda703657e33",
"accountingEntryId": "0c76ea71-aaaa-4ece-bb68-e1166ccaea04",
"status": "pending",
"externalId": null,
"externalUrl": null,
"failureReasonType": null,
"failureReason": null,
"exportedAt": null
}
],
"pagination": {
"hasPreviousPage": false,
"hasNextPage": false,
"currentRequestPagination": {
"sortingKeys": [],
"sortingOrder": [],
"parameters": {
"jobId": [
"8eb648ab-464b-42a0-ba17-eda703657e33"
]
}
},
"startCursor": "AAAAAADJ3YT4YKWVVLYA=AFRDZIJCOBE2JLCIOQDPOKFVZY",
"endCursor": "AAAAAADJ3YT4YKWTT6AA=4BWF5U5TLFGLNFMGIBLKZSXGGU",
"total": 4
}
}
```
### 2. Handle Pagination
Export Items may be returned across multiple pages.
Always retrieve all pages before beginning processing to avoid partial exports.
#### Example Response (Pagination)
```json theme={null}
"pagination": {
"hasNextPage": true,
"endCursor": ""
}
```
**Example Pseudo:**
```pseudo theme={null}
items = []
do:
response = fetchItems(cursor)
items.append(response.data)
cursor = response.pagination.endCursor
while response.pagination.hasNextPage
```
### 3. Handle Job Recovery
This step only applies if your integration is restarting after an interruption. In normal operation, skip to Step 4.
If your integration restarts or loses state, it must be able to resume processing safely.
#### 3.1. (Optional) Retrieve Export Job Metadata
This step is optional. If your integration already tracks job state, it can be skipped.
**API Endpoint**: GET [/v3/export-jobs/`{jobId}`](/reference/export-api/export-api-v3/get-an-export-job)
This endpoint can be used to rehydrate job-level context.
##### Example Request
```bash theme={null}
curl -X GET " https://external.staging.pleo.io/v3/export-jobs/8eb648ab-464b-42a0-ba17-eda703657e33" \
-H "Authorization: Bearer "
```
```bash theme={null}
curl --request GET \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
"https://external.staging.pleo.io/v3/export-jobs/8eb648ab-464b-42a0-ba17-eda703657e33" \
| jq
```
#### 3.2. Resume Processing from Export Items
If processing resumes after interruption:
* The Export Job is already `in_progress`
* Some Export Items may already have updated statuses
Your integration should:
1. Re-fetch all Export Items
2. Determine which items still require processing
3. Continue from the last known state
**Example Pseudo:**
```pseudo theme={null}
itemsToProcess = filter items where status == "pending"
```
This ensures the export workflow is resumable and idempotent.
### 4. Store Items for Processing
Persist Export Items for the processing phase.
Store only what is necessary:
* `accountingEntryId` (unique ID for the export item)
* `jobId` (unique job reference of the Export Job)
* `status` (status of the export item)
**Example Pseudo:**
```pseudo theme={null}
for item in items:
queue.push(item)
```
### 5. Maintain Deterministic Processing Order
Process Export Items in a stable and predictable order.
Recommended strategies:
* chronological order (by transaction date)
* consistent ordering across retries
Deterministic ordering improves reconciliation and debugging.
## Result
After completing these steps:
* All Export Items for the job have been retrieved
* Pagination has been fully resolved
* The workflow remains resumable and consistent
* The integration does not yet have the payload of each expense item
***
## What Comes Next?
Now that you have identified which items to process, retrieve their full accounting data.
How to Fetch Export Item Data for Processing
***
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# How to Transfer Attachments
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-transfer-attachments-for-as-erp-processing
This is not an error at the platform level. However, your integration must explicitly define whether attachments are:
* optional (continue processing), or
* required (fail the Export Item if missing).
### 2. Download Attachments
Iterate through all files:
```pseudo theme={null}
attachments = []
for file in files:
attachment = download(file.url)
attachments.append(attachment)
```
Guidelines:
* download immediately (URLs may expire)
* preserve file format and filename where possible
### 3. Handle Multiple Attachments
If multiple attachments exist:
```pseudo theme={null}
if accountingSystem.supportsMultipleAttachments:
upload all attachments
else:
mergedFile = merge(attachments)
upload mergedFile
```
Recommended merge strategies:
* PDF merge (preferred)
* ZIP archive (fallback)
### 4. Upload and Associate Attachments
Attach files to the created record:
* journal entry
* vendor invoice (AP)
* expense transaction
```pseudo theme={null}
for attachment in attachments:
uploadToAccountingSystem(entryId, attachment)
```
Ensure:
* attachments are linked to the correct entry
* traceability to the original Export Item is preserved
### 5. Handle Failures
Attachment failures must be handled explicitly.
```pseudo theme={null}
try:
download/upload
except transientError:
retry
except permanentError:
log failure
```
Recommended behaviour:
* retry transient failures (network/timeouts)
* log all failures
* decide whether to:
* fail the Export Item, or
* continue without attachment (system-dependent)
Do not silently discard attachments — always log or surface failures.
### 6. Ensure Idempotency
Avoid duplicate uploads when retrying exports.
```pseudo theme={null}
if attachmentAlreadyUploaded(file):
skip upload
```
Strategies:
* Generate a deterministic attachment key, for example:
* `accountingEntryId + file index`, or
* a hash of file metadata (e.g. URL, size, type)
* Store processed attachment keys to prevent duplicate uploads during retries
* Optionally check existing attachments in the Accounting System (if supported)
Attachment URLs may change across requests. Do not rely on URL alone as a unique identifier.
## Results
After completing this step:
* All available attachments have been:
* downloaded
* uploaded
* linked to accounting entries
* Attachments remain traceable to their originating Export Item
***
## What Comes Next?
How to Assign Accounting Periods
***
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# How to Update & Complete the Export Job
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-update-and-complete-export-job-for-as-erp-processing
After all Export Items have been processed and updated, you must complete the Export Job by sending a final status to Pleo.
This finalises the export workflow.
## Prerequisites
Before you begin:
* All Export Items have been processed in your AS/ERP
* [All Export Item statuses have been reported to Pleo](/docs/current/how-tos/accounting-integrations/how-to-update-export-items-for-as-erp-processing)
* You have the overall result of the export (success, partial success, or failure)
## Steps
### 1. Determine Final Outcome
Evaluate the results of all Export Items:
| Condition | Event |
| -------------------- | ----------------------- |
| All items successful | `completed` |
| Some items failed | `completed_with_errors` |
| All items failed | `failed` |
### 2. Build Event Payload
**All items successful**
```json theme={null}
{
"jobId": "JOB_ID",
"event": "completed"
}
```
**Partial success**
```json theme={null}
{
"jobId": "JOB_ID",
"event": "completed_with_errors"
}
```
**Fatal failure**
```json theme={null}
{
"jobId": "JOB_ID",
"event": "failed",
"failureReason": "ERP unavailable",
"failureReasonType": "service_unreachable"
}
```
Only include `failureReason` and `failureReasonType` when `"event": "failed"`. For `failureReasonType` guidance and supported values, see [Integration Design: Update & Complete Export Job](/docs/current/integration-design/exports/integration-design-exports-update-and-complete-export-job).
### 3. Send Event
```bash theme={null}
curl -X POST https://external.staging.pleo.io/v3/export-job-events \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"jobId": "JOB_ID",
"event": "completed"
}'
```
```bash theme={null}
curl --request POST \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v3/export-job-events" \
-d '{
"jobId": "JOB_ID",
"event": "completed"
}' | jq
```
### 4. Handle Response
```pseudo theme={null}
if response.success:
stopRetrying()
else:
retryOrLogError()
```
* Retry on transient failures
* Ensure the event is sent only once per final state
The Export Job should only be completed after all Export Item updates have been successfully acknowledged.
#### Pleo Web App
Completing the Export Job triggers the **user-visible update in Pleo**.
* Export status is shown in the **Export queue**
* Users can review failed items and error details
In the message banner at the top of your screen, click the **Review errors** link to see the reasons why Export Items failed.
## Result
After completing this step:
* Pleo marks the Export Job as completed based on the submitted event (`completed`, `completed_with_errors`, or `failed`)
* Export Job status is visible in the **Export queue**
* Export Item results (success and failure) are surfaced to users
* Failed items include error details for review and correction
* No further Export Item updates can be made for this job
Once an Export Job is completed, it is **final and cannot be modified**.
***
## What Comes Next?
Return to the Export Integration Workflow Guide
***
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# How to Update Export Items
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-to-update-export-items-for-as-erp-processing
## Prerequisites
Before you begin:
* You've [processed Export Items in your AS/ERP](/docs/current/how-tos/accounting-integrations/how-to-apply-posting-behaviour-for-as-erp-processing#5-create-entry-and-capture-result)
* You've captured a success or failure result for each item
## Constraints
* Maximum 100 items per API request
* Each item must include its accountingEntryId
* Supports both successful and failed updates
Export Item updates must be sent in batches of up to 100 items per request.
## Steps
### 1. Collect Results
For each processed Export Item, ensure you have:
* `accountingEntryId` (get this from [`/v3/export-items`](/docs/current/how-tos/accounting-integrations/how-to-fetch-export-item-data-for-as-erp-processing))
* result: **successful** or **failed** (get this from your [AS/ERP](/docs/current/how-tos/accounting-integrations/how-to-apply-posting-behaviour-for-as-erp-processing#5-create-entry-and-capture-result) system)
### 2. Build Update Payload
Use one of the supported [`status`](/reference/export-api/export-api-v3/update-export-job-items#body-items-status) parameters.
For field definitions (`externalId`, `externalUrl`, `exportedAt`), supported `failureReasonType` values, and common error mappings, see the [Integration Design: Update Export Items](/docs/current/integration-design/exports/integration-design-exports-update-export-items).
**Successful item**
```json theme={null}
{
"accountingEntryId": "ENTRY_ID",
"status": "successful",
"externalId": "JE-10452",
"externalUrl": "https://erp.example.com/journal/JE-10452"
}
```
**Failed item**
```json theme={null}
{
"accountingEntryId": "ENTRY_ID",
"status": "failed",
"failureReason": "Vendor not found in ERP",
"failureReasonType": "accounting_system_validation_failure"
}
```
Use one of the supported [`failureReasonTypes`](/reference/export-api/export-api-v3/update-export-job-items#body-items-failure-reason-type)
### 3. Batch Items
Group items into batches of up to 100:
```pseudo theme={null}
batches = chunk(items, 100)
```
### 4. Send Request
**API Endpoint**: PUT [v3/export-jobs/`{jobId}`/items](/reference/export-api/export-api-v3/update-export-job-items)
**Example parameters:**
* jobId: `8eb648ab-464b-42a0-ba17-eda703657e33`
* accountingEntryId: `59540ed2-0d68-4e36-9e31-58223975d9e9`
* accountingEntryId: `6678bcee-c6a4-4b49-bb1e-8f9fd653d16c`
* accountingEntryId: `9ea2ef81-4d73-460d-bbb4-ccf3da6f6f61`
* accountingEntryId: `0c76ea71-aaaa-4ece-bb68-e1166ccaea04`
```bash theme={null}
curl -X PUT https://external.staging.pleo.io/v3/export-jobs/8eb648ab-464b-42a0-ba17-eda703657e33/items \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '
[
{
"accountingEntryId": "59540ed2-0d68-4e36-9e31-58223975d9e9",
"status": "successful",
"exportedAt": "2026-04-22T10:08:56Z",
"externalId": "ext-1234",
"externalUrl": "ext-url"
},
{
"accountingEntryId": "6678bcee-c6a4-4b49-bb1e-8f9fd653d16c",
"status": "successful",
"exportedAt": "2026-04-22T10:08:56Z",
"externalId": "ext-5678",
"externalUrl": "ext-url"
},
{
"accountingEntryId": "9ea2ef81-4d73-460d-bbb4-ccf3da6f6f61",
"status": "successful",
"exportedAt": "2026-04-22T10:08:56Z",
"externalId": "ext-9012",
"externalUrl": "ext-url"
},
{
"accountingEntryId": "0c76ea71-aaaa-4ece-bb68-e1166ccaea04",
"status": "failed",
"exportedAt": "2026-04-22T10:08:56Z",
"failureReason": "Receipt corrupt",
"failureReasonType": "receipt_conversion_failure"
}
]'
```
```bash theme={null}
curl --request PUT \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v3/export-jobs/8eb648ab-464b-42a0-ba17-eda703657e33/items" \
-d '
[
{
"accountingEntryId": "59540ed2-0d68-4e36-9e31-58223975d9e9",
"status": "successful",
"exportedAt": "2026-04-22T10:08:56Z",
"externalId": "ext-1234",
"externalUrl": "ext-url"
},
{
"accountingEntryId": "6678bcee-c6a4-4b49-bb1e-8f9fd653d16c",
"status": "successful",
"exportedAt": "2026-04-22T10:08:56Z",
"externalId": "ext-5678",
"externalUrl": "ext-url"
},
{
"accountingEntryId": "9ea2ef81-4d73-460d-bbb4-ccf3da6f6f61",
"status": "successful",
"exportedAt": "2026-04-22T10:08:56Z",
"externalId": "ext-9012",
"externalUrl": "ext-url"
},
{
"accountingEntryId": "0c76ea71-aaaa-4ece-bb68-e1166ccaea04",
"status": "failed",
"exportedAt": "2026-04-22T10:08:56Z",
"failureReason": "Receipt corrupt",
"failureReasonType": "receipt_conversion_failure"
}
]
' | jq
```
### 5. Handle Response
```pseudo theme={null}
if response.success:
markBatchAsCompleted()
else:
retryOrLogError()
```
* Retry on transient failures
* Do not duplicate already successful updates
#### Example Response
```bash theme={null}
{
"data": [
{
"exportJobId": "f588b4db-a5cf-457c-b736-8b05dc6a2fdf",
"accountingEntryId": "59540ed2-0d68-4e36-9e31-58223975d9e9",
"status": "successful",
"externalId": "ext-1234",
"externalUrl": "ext-url",
"failureReasonType": null,
"failureReason": null,
"exportedAt": "2026-04-22T10:15:54Z"
},
{
"exportJobId": "f588b4db-a5cf-457c-b736-8b05dc6a2fdf",
"accountingEntryId": "6678bcee-c6a4-4b49-bb1e-8f9fd653d16c",
"status": "successful",
"externalId": "ext-5678",
"externalUrl": "ext-url",
"failureReasonType": null,
"failureReason": null,
"exportedAt": "2026-04-22T10:15:54Z"
},
{
"exportJobId": "f588b4db-a5cf-457c-b736-8b05dc6a2fdf",
"accountingEntryId": "9ea2ef81-4d73-460d-bbb4-ccf3da6f6f61",
"status": "successful",
"externalId": "ext-9012",
"externalUrl": "ext-url",
"failureReasonType": null,
"failureReason": null,
"exportedAt": "2026-04-22T10:15:54Z"
},
{
"exportJobId": "f588b4db-a5cf-457c-b736-8b05dc6a2fdf",
"accountingEntryId": "0c76ea71-aaaa-4ece-bb68-e1166ccaea04",
"status": "failed",
"externalId": null,
"externalUrl": null,
"failureReasonType": "receipt_conversion_failure",
"failureReason": "Receipt corrupt",
"exportedAt": null
}
],
"errors": []
}
```
#### Pleo Web App
Export Item results are recorded in Pleo but are **not fully surfaced to users until the Export Job is completed**.
Once the Export Job is finalised:
* Export status becomes visible in the **Export queue**
* Failed items and error details are available for review
## Result
After completing this step:
* The job remains in the queue for auditing and compliance purposes
* In Pleo's Web App:
* Export progress is accurately reflected in the **Export queue** tab
* Errors are visible to users for review and correction
Each time you click **Export**, a new job is created in the export queue with a unique `jobId`.
***
## What Comes Next?
How to Update and Complete an Export Job
***
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# How-to Catalogue
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/how-tos-overview
Step-by-step articles for building and testing accounting integrations with Pleo. Select a category to get started.
## Exports
The end-to-end articles for building an export integration, from queuing expenses in the Pleo Web App through to completing an Export Job. Follow these in sequence when building your integration for the first time.
Articles covering the AS/ERP processing phase: bookkeeping method resolution, accounts and data mapping, attachment handling, accounting periods, and posting behaviour.
Targeted articles for resolving common integration issues. Come here when a specific step in the export flow is failing or behaving unexpectedly.
## Imports
Articles for implementing Tags Sync: selecting Dimensions, syncing Tag Groups with Dimensions, and syncing Tags with Dimension Values.
Articles for implementing Chart of Accounts Sync: fetching and matching accounts, then creating, updating, unarchiving, and archiving accounts in Pleo.
Articles for implementing Tax Sync: fetching and matching tax codes, then creating, updating, unarchiving, and archiving Tax Codes in Pleo.
# How to Create Multiple Accounts in a Single Request
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-batch-create-accounts
The Chart of Accounts API supports creating up to 1000 Accounts in a single request using the batch endpoint. This is more efficient than individual creates when your sync produces a large number of new accounts, and helps keep API call volume within rate limit targets.
This how-to covers the batch create step only. For update, unarchive, and archive operations, use the individual PUT endpoint as described in [How to Create, Update, and Archive Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-create-update-archive-accounts).
## When to Use This
The batch endpoint is useful when:
* Running an initial sync where many accounts need to be created from scratch.
* A large number of new accounts have been added to the AS since the last sync cycle.
For small numbers of new accounts, the single-create endpoint works fine.
## Prerequisites
Before you begin:
* You have completed [How to Sync Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts) and identified which accounts need to be created in Pleo.
* Your integration is authenticated using one of the [supported authentication methods](/docs/current/integration-design/auth/integration-design-auth-overview#authentication-policy-overview).
* Your integration can call Pleo's Chart of Accounts API endpoints.
## Steps
### 1. Build the Batch Request
**API Endpoint**: POST [`/v1/chart-of-accounts/batch`](/reference/accounts/create-multiple-accounts-in-a-single-request)
Collect all accounts that need to be created and include them as `items` in the request body. Each item requires `externalId`, `name`, and `archived`. The `code` and `taxCodeExternalId` fields are optional.
A single request can include between 1 and 1000 accounts. If you have more than 1000 accounts to create, split them across multiple requests.
**Example Pseudo:**
```pseudo theme={null}
newAccounts = [account for account in activeASAccounts if not matchedInPleo]
for batch in chunks(newAccounts, size=1000):
items = []
for account in batch:
items.append({
externalId: account.externalId,
code: account.code,
name: account.name,
archived: false
})
POST /v1/chart-of-accounts/batch with { companyId, items }
```
#### Example Request
```bash theme={null}
curl -X POST "https://external.staging.pleo.io/v1/chart-of-accounts/batch" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"items": [
{
"externalId": "ext-3000",
"code": "3000",
"name": "Software",
"archived": false
},
{
"externalId": "ext-6000",
"code": "6000",
"name": "Consulting",
"archived": false
},
{
"externalId": "ext-7000",
"code": "7000",
"name": "Equipment",
"archived": false
}
]
}'
```
```bash theme={null}
curl --request POST \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v1/chart-of-accounts/batch" \
-d '{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"items": [
{
"externalId": "ext-3000",
"code": "3000",
"name": "Software",
"archived": false
},
{
"externalId": "ext-6000",
"code": "6000",
"name": "Consulting",
"archived": false
},
{
"externalId": "ext-7000",
"code": "7000",
"name": "Equipment",
"archived": false
}
]
}' \
| jq
```
***
### 2. Handle the Response
The API returns a `201` even when some items fail. Always check both the `created` and `failed` arrays in the response.
#### Example Response
```json theme={null}
{
"data": {
"created": [
{
"id": "f1a2b3c4-d5e6-7f89-abcd-ef1234567890",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"externalId": "ext-3000",
"code": "3000",
"name": "Software",
"archived": false
},
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567891",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"externalId": "ext-6000",
"code": "6000",
"name": "Consulting",
"archived": false
},
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678912",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"externalId": "ext-7000",
"code": "7000",
"name": "Equipment",
"archived": false
}
],
"failed": []
}
}
```
#### Handling Failures
Items that fail validation are returned in the `failed` array with a `reasons` field explaining what went wrong. Successfully validated items are still created: the batch does not roll back.
```json theme={null}
{
"data": {
"created": [...],
"failed": [
{
"reasons": ["EXTERNAL_ID_ALREADY_EXISTS"],
"request": {
"externalId": "ext-3000",
"code": "3000",
"name": "Software",
"archived": false
}
}
]
}
}
```
A `201` response does not mean all accounts were created. Always inspect the `failed` array and handle any failures, for example by logging them or retrying with corrected data.
***
## Related Reading
* [How to Create, Update, and Archive Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-create-update-archive-accounts)
* [How to Sync Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts)
* [Chart of Accounts Sync Periodicity and Scheduling](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-periodicity)
***
# How to Create, Update, and Archive Accounts
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-create-update-archive-accounts
This how-to covers the write operations for Chart of Accounts Sync: creating new Accounts, updating or unarchiving existing Accounts, and archiving Accounts that are no longer active in the Accounting System.
Run this after [How to Sync Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts), which covers fetching and matching accounts from both systems.
## Prerequisites
Before you begin:
* You have completed [How to Sync Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts) and determined what action is needed for each account
* Your integration is authenticated using one of the [supported authentication methods](/docs/current/integration-design/auth/integration-design-auth-overview#authentication-policy-overview)
* Your integration can call Pleo's Chart of Accounts API endpoints
## Scenario
This how-to continues from the [How to Sync Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts) scenario. After matching, the following actions are required:
| AS Account | Pleo Account | Action |
| ---------------------- | ---------------------- | --------- |
| 1000 - Office Supplies | 1000 - Office Supplies | No action |
| 2000 - Travel | 2000 - Travel | Unarchive |
| 3000 - Software | — | Create |
| 4000 - Marketing | 4000 - Advertising | Update |
| — | 5000 - Entertainment | Archive |
The diagram below shows where these operations fit in the full reconciliation loop.
## Steps
### 1. Create Accounts for New AS Entries
**API Endpoint**: POST [`/v1/chart-of-accounts`](/reference/accounts/create-a-new-account)
If an AS account has no matching Pleo Account, create a new Account.
**Example Pseudo:**
```pseudo theme={null}
if matchedAccount is null:
newAccount.externalId = account.externalId
newAccount.code = account.code
newAccount.name = account.name
newAccount.archived = false
POST newAccount to Pleo
```
#### Example Request
```bash theme={null}
curl -X POST "https://external.staging.pleo.io/v1/chart-of-accounts" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"externalId": "ext-3000",
"code": "3000",
"name": "Software",
"archived": false
}'
```
```bash theme={null}
curl --request POST \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v1/chart-of-accounts" \
-d '{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"externalId": "ext-3000",
"code": "3000",
"name": "Software",
"archived": false
}' \
| jq
```
#### Example Response
```json theme={null}
{
"data": {
"id": "e5f6g7h8-i901-234i-5678-efg90i12defg",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"externalId": "ext-3000",
"code": "3000",
"name": "Software",
"archived": false
}
}
```
#### What it looks like in Pleo Web App
If you have a large number of accounts to create, you can send up to 1000 in a single request using the batch endpoint. See [How to Create Multiple Accounts in a Single Request](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-batch-create-accounts).
***
### 2. Unarchive or Update Existing Accounts
**API Endpoint**: PUT [`/v1/chart-of-accounts/{accountId}`](/reference/accounts/update-an-account)
If a matching Account is found:
* **AS account is active, Pleo Account is archived:** Unarchive by setting `archived: false` and update name and code if they differ.
* **AS account is active, Pleo Account name or code differs:** Update the Account to match the AS.
* **AS account is active, Pleo Account name and code match:** No action required.
**Example Pseudo:**
```pseudo theme={null}
if matchedAccount.archived == true:
matchedAccount.archived = false
matchedAccount.name = account.name
matchedAccount.code = account.code
PUT matchedAccount to Pleo
else if matchedAccount.name != account.name or matchedAccount.code != account.code:
matchedAccount.name = account.name
matchedAccount.code = account.code
PUT matchedAccount to Pleo
```
### Unarchive
#### Example Request
The "2000 - Travel" Account is unarchived in this example.
```bash theme={null}
curl -X PUT "https://external.staging.pleo.io/v1/chart-of-accounts/d8371c2d-3f62-47df-a8d6-6a3ba6387e00" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"externalId": "ext-2000",
"code": "2000",
"name": "Travel",
"archived": false
}'
```
```bash theme={null}
curl --request PUT \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v1/chart-of-accounts/d8371c2d-3f62-47df-a8d6-6a3ba6387e00" \
-d '{
"externalId": "ext-2000",
"code": "2000",
"name": "Travel",
"archived": false
}' \
| jq
```
#### Example Response
```json theme={null}
{
"data": {
"id": "d8371c2d-3f62-47df-a8d6-6a3ba6387e00",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"externalId": "ext-2000",
"code": "2000",
"name": "Travel",
"archived": false
}
}
```
#### What it looks like in Pleo Web App
### Update
#### Example Request
The "4000 - Advertising" Account is renamed to "4000 - Marketing" to match the AS in this example.
```bash theme={null}
curl -X PUT "https://external.staging.pleo.io/v1/chart-of-accounts/a470bfbe-8046-4b43-a4d9-4f5796acdd93" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"externalId": "ext-4000",
"code": "4000",
"name": "Marketing",
"archived": false
}'
```
```bash theme={null}
curl --request PUT \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v1/chart-of-accounts/a470bfbe-8046-4b43-a4d9-4f5796acdd93" \
-d '{
"externalId": "ext-4000",
"code": "4000",
"name": "Marketing",
"archived": false
}' \
| jq
```
#### Example Response
```json theme={null}
{
"data": {
"id": "a470bfbe-8046-4b43-a4d9-4f5796acdd93",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"externalId": "ext-4000",
"code": "4000",
"name": "Marketing",
"archived": false
}
}
```
#### What it looks like in Pleo Web App
Before update:
After Update:
***
### 3. Archive Accounts with No Matching AS Entry
**API Endpoint**: PUT [`/v1/chart-of-accounts/{accountId}`](/reference/accounts/update-an-account)
If a Pleo Account is active but its corresponding AS account is no longer active (or does not exist), archive the Account.
Do not delete Accounts. Archiving is non-destructive and reversible.
**Example Pseudo:**
```pseudo theme={null}
activeASExternalIds = activeASAccounts.map(a => a.externalId)
for account in pleoAccounts where archived == false:
if account.externalId not in activeASExternalIds:
account.archived = true
PUT account to Pleo
```
#### Example Request
```bash theme={null}
curl -X PUT "https://external.staging.pleo.io/v1/chart-of-accounts/7ea2006f-180e-4c52-a2bb-562d3a62fd48" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"externalId": "5000",
"code": "5000",
"name": "Entertainment",
"archived": true
}'
```
```bash theme={null}
curl --request PUT \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v1/chart-of-accounts/7ea2006f-180e-4c52-a2bb-562d3a62fd48" \
-d '{
"externalId": "5000",
"code": "5000",
"name": "Entertainment",
"archived": true
}' \
| jq
```
As this is a PUT action, include the current values for all fields; only `archived` should change.
#### Example Response
```json theme={null}
{
"data": {
"id": "7ea2006f-180e-4c52-a2bb-562d3a62fd48",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"externalId": "5000",
"code": "5000",
"name": "Entertainment",
"archived": true
}
}
```
#### What it looks like in Pleo Web App
***
## Result
The table below recaps what happened to each Account across all steps.
| Account | AS Status | Pleo State Before | Action | Final Pleo State |
| ---------------------- | --------- | --------------------- | ---------- | ---------------- |
| 1000 - Office Supplies | Active | Active, matches | No action | Active |
| 2000 - Travel | Active | Archived | Unarchived | Active |
| 3000 - Software | Active | Does not exist | Created | Active |
| 4000 - Marketing | Active | Active, name differed | Updated | Active |
| 5000 - Entertainment | Not in AS | Active | Archived | Archived |
Pleo now reflects the current Chart of Accounts from the AS.
| Account | Final State in Pleo | In AS? | Aligned? |
| ---------------------- | ------------------- | ------ | :------: |
| 1000 - Office Supplies | Active | Yes | ✓ Yes |
| 2000 - Travel | Active | Yes | ✓ Yes |
| 3000 - Software | Active | Yes | ✓ Yes |
| 4000 - Marketing | Active | Yes | ✓ Yes |
| 5000 - Entertainment | Archived | No | ✓ Yes |
***
## What Comes Next?
Chart of Accounts Sync Workflow Guide
***
***
## Related Reading
* [How to Sync Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts)
* [Sync Accounts Integration Design](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-sync)
* [Data Mapping](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-data-mapping)
* [Platform Capabilities: Chart of Accounts Sync](/docs/current/platform/accounting-integrations/imports/accounts/accounts-sync-overview)
***
# How to Sync Accounts
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts
This how-to covers the fetch and match phase of Chart of Accounts Sync: retrieving accounts from the Accounting System and Pleo, then determining what action is needed for each. For the write operations, see [How to Create, Update, and Archive Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-create-update-archive-accounts).
Your integration must:
* Retrieve active accounts from the AS
* Retrieve all Accounts (active and archived) from Pleo
* Match AS accounts to Pleo Accounts by `externalId` to determine what action is needed
## Prerequisites
Before you begin:
* You're familiar with the [Chart of Accounts Sync Overview](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-overview) and the [Integration Design for Syncing Accounts](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-sync)
* Your integration is authenticated using one of the [supported authentication methods](/docs/current/integration-design/auth/integration-design-auth-overview#authentication-policy-overview)
* Your integration can call Pleo's Chart of Accounts API endpoints
## Scenario
This how-to uses a consistent example to illustrate each step. The table below shows the starting state in both systems before this sync runs.
| Account (AS) | externalId | AS Status | Account (Pleo) | Pleo Status |
| ---------------------- | ---------- | --------- | ---------------------- | -------------- |
| 1000 - Office Supplies | ext-1000 | Active | 1000 - Office Supplies | Active |
| 2000 - Travel | ext-2000 | Active | 2000 - Travel | Archived |
| 3000 - Software | ext-3000 | Active | — | Does not exist |
| 4000 - Marketing | ext-4000 | Active | 4000 - Advertising | Active |
| — | 5000 | — | 5000 - Entertainment | Active |
## Steps
### 1. Retrieve Active Accounts from the Accounting System
Fetch all active accounts from the Chart of Accounts in the AS.
**Example Pseudo:**
```pseudo theme={null}
activeASAccounts = fetchActiveAccountsFromAS()
```
#### Example Result
| Account | externalId | AS Status |
| ---------------------- | ---------- | --------- |
| 1000 - Office Supplies | ext-1000 | Active |
| 2000 - Travel | ext-2000 | Active |
| 3000 - Software | ext-3000 | Active |
| 4000 - Marketing | ext-4000 | Active |
***
### 2. Retrieve All Accounts from Pleo
**API Endpoint**: POST [`/v1/chart-of-accounts:search`](/reference/accounts/fetch-a-list-of-accounts)
**Example parameters:**
* companyId: `12abc3d4-e567-890e-1234-abc56e78fabc`
* includeArchived: `true`
Fetch both active and archived Accounts from Pleo. Including archived Accounts allows unarchiving rather than creating duplicates.
**Example Pseudo:**
```pseudo theme={null}
pleoAccounts = fetchAccountsFromPleo(includeArchived: true)
```
#### Example Request
```bash theme={null}
curl -X POST "https://external.staging.pleo.io/v1/chart-of-accounts:search" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"includeArchived": true
}'
```
```bash theme={null}
curl --request POST \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v1/chart-of-accounts:search" \
-d '{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"includeArchived": true
}' \
| jq
```
#### Example Response
```json theme={null}
{
"data": [
{
"id": "7ea2006f-180e-4c52-a2bb-562d3a62fd48",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"code": "5000",
"externalId": "5000",
"name": "Entertainment",
"archived": false
},
{
"id": "a470bfbe-8046-4b43-a4d9-4f5796acdd93",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"code": "4000",
"externalId": "ext-4000",
"name": "Advertising",
"archived": false
},
{
"id": "d8371c2d-3f62-47df-a8d6-6a3ba6387e00",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"code": "2000",
"externalId": "ext-2000",
"name": "Travel",
"archived": true
},
{
"id": "77049275-e102-4ce4-84e5-c90852765fcd",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"code": "1000",
"externalId": "ext-1000",
"name": "Office Supplies",
"archived": false
}
],
"pagination": {
"hasPreviousPage": false,
"hasNextPage": false,
"currentRequestPagination": {
"sortingKeys": [],
"sortingOrder": [],
"parameters": {}
},
"startCursor": "AAAAAADKHEEOIDOJO34A=AAAAAADKHEEOIDOJO34A=P2RAA3YYBZGFFIV3KYWTUYX5JA",
"endCursor": "AAAAAADKCVM2UIAPLIEA=AAAAAADKHEETOMBB2K4A=BLNKLWE6ZBBDLLU24XGZ4GAFAI",
"total": 4
}
}
```
#### Example Result
| Account (Pleo) | externalId | Pleo Status |
| ---------------------- | ---------- | ----------- |
| 1000 - Office Supplies | ext-1000 | Active |
| 2000 - Travel | ext-2000 | Archived |
| 4000 - Advertising | ext-4000 | Active |
| 5000 - Entertainment | 5000 | Active |
#### What it looks like in Pleo Web App
***
### 3. Match AS Accounts to Pleo Accounts by externalId
For each active AS account, attempt to find a matching Account in Pleo using `externalId`.
**Example Pseudo:**
```pseudo theme={null}
pleoAccountsByExternalId = index pleoAccounts by externalId
for account in activeASAccounts:
matchedAccount = pleoAccountsByExternalId[account.externalId]
if matchedAccount is null:
createAccount(account)
else if matchedAccount.archived == true:
unarchiveAndUpdate(matchedAccount, account)
else if matchedAccount.name != account.name or matchedAccount.code != account.code:
updateAccount(matchedAccount, account)
// else: no action needed
```
#### Example Result
| AS Account | Pleo Account | Pleo Status | Action |
| ---------------------- | ---------------------- | --------------- | --------- |
| 1000 - Office Supplies | 1000 - Office Supplies | Active, matches | No action |
| 2000 - Travel | 2000 - Travel | Archived | Unarchive |
| 3000 - Software | — | Does not exist | Create |
| 4000 - Marketing | 4000 - Advertising | Active, differs | Update |
| — | 5000 - Entertainment | Active | Archive |
***
## What Comes Next?
How to Create, Update, and Archive Accounts
***
***
## Related Reading
* [How to Create, Update, and Archive Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-create-update-archive-accounts)
* [Sync Accounts Integration Design](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-sync)
* [Data Mapping](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-data-mapping)
* [Platform Capabilities: Chart of Accounts Sync](/docs/current/platform/accounting-integrations/imports/accounts/accounts-sync-overview)
***
# How to Select Dimensions for Tags Sync
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/imports/tags/how-to-select-dimensions-for-tags-sync
This how-to explains how an integration configures which Accounting Dimensions from the Accounting System are synchronised as Tag Groups in Pleo.
Selecting Dimensions is the first step in the Tags Sync workflow and determines the scope of every subsequent sync cycle.
Your integration must:
* Retrieve active Dimensions from the Accounting System
* Retrieve existing Tag Groups from Pleo
* Apply auto-selection logic on first connection
* Store the selection for use in recurring sync cycles
## Prerequisites
Before you begin:
* You're familiar with the [Tags Sync Overview](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-overview) and the [Integration Design for Dimension Selection](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-dimension-selection)
* Your integration is authenticated using one of the [supported authentication methods](/docs/current/integration-design/auth/integration-design-auth-overview#authentication-policy-overview)
* Your integration can call Pleo's Tags API endpoints
## Scenario
This how-to uses a concrete example to illustrate each step.
The user is connecting their integration to Pleo for the first time. In their Accounting System, they have manually selected **Finance**, **Cost Centre**, and **Supplier** for synchronisation. The integration will also auto-select **Department** and **Project** because those codes already match active Tag Groups in Pleo.
After the initial connection, **Supplier** becomes inactive in the AS. On the next scheduled sync cycle, step 5 detects this and automatically deselects it.
The table below shows the starting state in both systems before the sync process runs.
| Dimension (AS) | AS Status | Tag Group (Pleo) | Pleo Status |
| -------------- | --------- | ---------------- | -------------- |
| Department | Active | Department | Active |
| Project | Active | Project | Active |
| Finance | Active | — | Does not exist |
| Cost Centre | Active | Cost Centre | Archived |
| Supplier | Active | — | Does not exist |
| — | — | Region | Active |
***
## Steps
### 1. Retrieve Active Dimensions from the Accounting System
Fetch all active (non-archived, non-inactive) Dimensions from the Accounting System.
Each Dimension must have:
* A unique **code**
* A human-readable **name**
**Example Pseudo:**
```pseudo theme={null}
dimensions = fetchDimensionsFromAS()
activeDimensions = filter dimensions where status == "active"
if activeDimensions is empty:
exit workflow
```
#### Example Response
The response format depends on your Accounting System. The following is a representative example:
```json theme={null}
{
"dimensions": [
{ "code": "Department", "name": "Department", "status": "active" },
{ "code": "Project", "name": "Project", "status": "active" },
{ "code": "Finance", "name": "Finance", "status": "active" },
{ "code": "Cost Centre", "name": "Cost Centre", "status": "active" },
{ "code": "Supplier", "name": "Supplier", "status": "active" }
]
}
```
After filtering: `activeDimensions` = Department, Project, Finance, Cost Centre, Supplier
***
### 2. Retrieve Existing Tag Groups from Pleo
**API Endpoint**: GET [`/v0/tag-groups`](/reference/tags-api/tag-groups/returns-all-tag-groups-for-given-company)
**Example parameters:**
* companyId: `12abc3d4-e567-890e-1234-abc56e78fabc`
* includeArchived: `false`
Fetch active Tag Groups from Pleo. These are used to identify which Dimensions can be auto-selected on first connection.
**Example Pseudo:**
```pseudo theme={null}
tagGroups = fetchTagGroupsFromPleo(includeArchived: false)
```
#### Example Request
```bash theme={null}
curl -X GET "https://external.staging.pleo.io/v0/tag-groups?company_id=12abc3d4-e567-890e-1234-abc56e78fabc&include_archived=false" \
-H "Authorization: Bearer "
```
```bash theme={null}
curl --request GET \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
"https://external.staging.pleo.io/v0/tag-groups?company_id=12abc3d4-e567-890e-1234-abc56e78fabc&include_archived=false" \
| jq
```
#### Example Response
```json theme={null}
{
"data": [
{
"id": "768fb809-b282-4411-875a-406f8e4c5bdb",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "Department",
"code": "Department",
"archived": false,
"createdAt": "2026-05-21T13:19:05.124301Z",
"updatedAt": "2026-05-21T13:19:05.124301Z",
"metadata": {}
},
{
"id": "a9a0a2f5-a97b-41f9-aa28-dc3cde85f691",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "Project",
"code": "Project",
"archived": false,
"createdAt": "2026-05-21T13:19:43.112716Z",
"updatedAt": "2026-05-21T13:19:43.112716Z",
"metadata": {}
},
{
"id": "f1cd0c96-32c5-4b85-8b93-8f6543bc6378",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "Region",
"code": "Region",
"archived": false,
"createdAt": "2026-05-21T14:12:44.543271Z",
"updatedAt": "2026-05-21T14:12:44.543271Z",
"metadata": {}
}
]
}
```
Cost Centre also exists in Pleo but is currently archived. Archived Tag Groups are not included in this response; they do not factor into auto-selection matching, but are relevant in the next step when syncing Tag Groups.
***
### 3. Apply Auto-Selection on First Connection
On first connection, automatically pre-select Dimensions whose code matches an existing active Tag Group in Pleo.
Matching is **case-insensitive**.
Up to **5 Dimensions** may be selected in total.
**Example Pseudo:**
```pseudo theme={null}
activeTagGroups = filter tagGroups where archived == false
tagGroupCodes = activeTagGroups.map(code.toLowerCase())
autoSelected = []
for dimension in activeDimensions:
if dimension.code.toLowerCase() in tagGroupCodes:
autoSelected.append(dimension)
selectedDimensions = autoSelected.take(5)
```
#### Example Auto-Selection Result
Using the data from steps 1 and 2:
| AS Dimension | Before Connection | Active Pleo Tag Group? | Auto-Selected | Now Selected |
| ------------ | ----------------- | ------------------------ | :-----------: | :----------: |
| Department | Not selected | ✓ Matches "Department" | ✓ Yes | ✓ Yes |
| Project | Not selected | ✓ Matches "Project" | ✓ Yes | ✓ Yes |
| Finance | Manually selected | ✗ Does not exist in Pleo | — | ✓ Yes |
| Cost Centre | Manually selected | ✗ Tag Group is archived | — | ✓ Yes |
| Supplier | Manually selected | ✗ Does not exist in Pleo | — | ✓ Yes |
Department and Project are auto-selected. Finance, Cost Centre, and Supplier were already manually selected before connecting; this step does not change their selection state.
The full selection entering step 4 is: **Department, Project, Finance, Cost Centre, Supplier**.
Auto-selection only runs on first connection. On subsequent connections, the existing selection is restored from your integration's stored configuration.
***
### 4. Store the Dimension Selection
Persist the selected Dimensions in your integration's configuration store.
The stored selection is used as input to every Tags Sync cycle (Steps 2 and 3).
**Example Pseudo:**
```pseudo theme={null}
storeSelectedDimensions(selectedDimensions)
```
#### Example Stored Selection
| Dimension | Source |
| ----------- | ----------------- |
| Department | Auto-selected |
| Project | Auto-selected |
| Finance | Manually selected |
| Cost Centre | Manually selected |
| Supplier | Manually selected |
A maximum of 5 Dimensions may be selected at any time. If the user attempts to select more than 5, reject the additional selection.
***
### 5. Handle Inactive Dimensions
If a previously selected Dimension is no longer active in the AS, automatically deselect it during the next sync cycle.
**Example Pseudo:**
```pseudo theme={null}
for selectedDimension in storedSelectedDimensions:
if selectedDimension not in activeDimensions:
deselect(selectedDimension)
```
This ensures the selection remains consistent with the current state of the AS without requiring manual user intervention.
#### Example Result
Supplier was active at the time of the initial connection but has since become inactive in the AS. On this sync cycle it is detected and deselected:
| Dimension | AS Status | Deselected |
| ----------- | --------- | ---------- |
| Department | Active | No |
| Project | Active | No |
| Finance | Active | No |
| Cost Centre | Active | No |
| Supplier | Inactive | ✓ Yes |
Final stored selection: **Department**, **Project**, **Finance**, **Cost Centre**
***
## Result
The table below shows what happened to each Dimension across all steps.
| Dimension | Retrieved (Step 1) | Auto-Selected (Step 3) | Stored (Step 4) | Step 5 | Final Selection |
| ----------- | ------------------ | ---------------------- | :-------------: | --------------------- | :-------------: |
| Department | Active | ✓ Yes | ✓ Yes | Active (no change) | ✓ Yes |
| Project | Active | ✓ Yes | ✓ Yes | Active (no change) | ✓ Yes |
| Finance | Active | Manually selected | ✓ Yes | Active (no change) | ✓ Yes |
| Cost Centre | Active | Manually selected | ✓ Yes | Active (no change) | ✓ Yes |
| Supplier | Active | Manually selected | ✓ Yes | Inactive (deselected) | ✗ No |
The final selection is stored and ready for the **Tag Group Sync** cycle:
* Department
* Project
* Finance
* Cost Centre
***
## What Comes Next?
How to Sync Tag Groups with Accounting Dimensions
***
***
## Related Reading
* [Tags Sync Overview](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-overview)
* [Dimension Selection](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-dimension-selection)
* [Platform Capabilities — Tags Sync](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview)
***
# How to Sync Tag Groups with Accounting Dimensions
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/imports/tags/how-to-sync-tag-groups-with-accounting-dimensions
This how-to explains how an integration synchronises Tag Groups in Pleo with the selected Accounting Dimensions from the Accounting System.
Syncing Tag Groups is the second step in the Tags Sync workflow.
Tag Groups in Pleo correspond to Dimensions in the Accounting System. This step ensures the Tag Group structure in Pleo mirrors the current state of the selected Dimensions.
Your integration must:
* Retrieve selected Dimensions from the AS
* Retrieve all Tag Groups (active and archived) from Pleo
* Create, unarchive, update, or archive Tag Groups as needed
* Deselect Dimensions that have become inactive in the AS
## Prerequisites
Before you begin:
* You're familiar with the [Tags Sync Overview](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-overview) and the [Integration Design for Syncing Tag Groups](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tag-groups)
* Your integration is authenticated using one of the [supported authentication methods](/docs/current/integration-design/auth/integration-design-auth-overview#authentication-policy-overview)
* Your integration can call Pleo's Tags API endpoints
* [Dimension selection has been configured](/docs/current/how-tos/accounting-integrations/imports/tags/how-to-select-dimensions-for-tags-sync)
## Scenario
This how-to continues from [How to Select Dimensions for Tags Sync](/docs/current/how-tos/accounting-integrations/imports/tags/how-to-select-dimensions-for-tags-sync).
The final Dimension selection from that step (**Department**, **Project**, **Finance**, and **Cost Centre**) is the input for this sync. The table below shows the starting state in both systems when this sync runs.
| Dimension (AS) | Selected | Tag Group (Pleo) | Pleo Status |
| -------------- | -------- | ---------------- | -------------- |
| Department | ✓ Yes | Department | Active |
| Project | ✓ Yes | Project | Active |
| Finance | ✓ Yes | — | Does not exist |
| Cost Centre | ✓ Yes | Cost Centre | Archived |
| — | — | Region | Active |
## Steps
### 1. Retrieve Selected Dimensions from the Accounting System
Fetch all Dimensions from the AS that are currently selected in your integration's configuration.
For each Dimension, verify it is still active. Deselect any that are no longer active.
**Example Pseudo:**
```pseudo theme={null}
selectedDimensions = loadSelectedDimensionsFromConfig()
activeDimensions = fetchDimensionsFromAS()
for dimension in selectedDimensions:
if dimension not in activeDimensions:
deselect(dimension)
selectedDimensions = loadSelectedDimensionsFromConfig()
```
#### Example Result
All four selected Dimensions are still active in the AS. None are deselected.
| Dimension | AS Status | Retained? |
| ----------- | --------- | :-------: |
| Department | Active | ✓ Yes |
| Project | Active | ✓ Yes |
| Finance | Active | ✓ Yes |
| Cost Centre | Active | ✓ Yes |
***
### 2. Retrieve All Tag Groups from Pleo
**API Endpoint**: GET [`/v0/tag-groups`](/reference/tags-api/tag-groups/returns-all-tag-groups-for-given-company)
**Example parameters:**
* companyId: `12abc3d4-e567-890e-1234-abc56e78fabc`
* includeArchived: `true`
Fetch both active and archived Tag Groups from Pleo. Including archived Tag Groups allows unarchiving rather than creating duplicates.
**Example Pseudo:**
```pseudo theme={null}
tagGroups = fetchTagGroupsFromPleo(includeArchived: true)
```
#### Example Request
```bash theme={null}
curl -X GET "https://external.staging.pleo.io/v0/tag-groups?company_id=12abc3d4-e567-890e-1234-abc56e78fabc&include_archived=true" \
-H "Authorization: Bearer "
```
```bash theme={null}
curl --request GET \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
"https://external.staging.pleo.io/v0/tag-groups?company_id=12abc3d4-e567-890e-1234-abc56e78fabc&include_archived=true" \
| jq
```
#### Example Response
```json theme={null}
{
"data": [
{
"id": "faf59f4a-073e-4f23-9725-944d993e6310",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "Cost Centre",
"code": "Cost Centre",
"archived": true,
"createdAt": "2026-05-21T13:23:03.729251Z",
"updatedAt": "2026-05-21T13:23:14.461704Z",
"metadata": {}
},
{
"id": "768fb809-b282-4411-875a-406f8e4c5bdb",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "Department",
"code": "Department",
"archived": false,
"createdAt": "2026-05-21T13:19:05.124301Z",
"updatedAt": "2026-05-21T13:19:05.124301Z",
"metadata": {}
},
{
"id": "a9a0a2f5-a97b-41f9-aa28-dc3cde85f691",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "Project",
"code": "Project",
"archived": false,
"createdAt": "2026-05-21T13:19:43.112716Z",
"updatedAt": "2026-05-21T13:19:43.112716Z",
"metadata": {}
},
{
"id": "f1cd0c96-32c5-4b85-8b93-8f6543bc6378",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "Region",
"code": "Region",
"archived": false,
"createdAt": "2026-05-21T14:12:44.543271Z",
"updatedAt": "2026-05-21T14:12:44.543271Z",
"metadata": {}
}
]
}
```
#### What it looks like in Pleo Web App
#### Example Result
| Tag Group | Pleo Status |
| ----------- | ----------- |
| Department | Active |
| Project | Active |
| Region | Active |
| Cost Centre | Archived |
***
### 3. Match Dimensions to Tag Groups by Code
For each selected Dimension, attempt to find a matching Tag Group in Pleo using **code** (case-insensitive).
**Example Pseudo:**
```pseudo theme={null}
tagGroupsByCode = index tagGroups by code.toLowerCase()
for dimension in selectedDimensions:
matchedTagGroup = tagGroupsByCode[dimension.code.toLowerCase()]
if matchedTagGroup is null:
createTagGroup(dimension)
else if matchedTagGroup.archived == true:
unarchiveTagGroup(matchedTagGroup, dimension)
else:
updateTagGroupIfNameChanged(matchedTagGroup, dimension)
```
#### Example Result
| AS Dimension | Tag Group (Pleo) | Pleo Status | Action |
| ------------ | ---------------- | -------------- | ------------------- |
| Department | Department | Active | No action |
| Project | Project | Active | No action |
| Finance | — | Does not exist | → Step 4: Create |
| Cost Centre | Cost Centre | Archived | → Step 5: Unarchive |
| — | Region | Active | → Step 6: Archive |
***
### 4. Create Tag Groups for New Dimensions
**API Endpoint**: POST [`/v0/tag-groups`](/reference/tags-api/tag-groups/creates-a-new-tag-group-resource)
If an AS Dimension is selected but no matching Pleo Tag Group exists, create a new Tag Group.
This only applies when no Tag Group with this code exists in Pleo (neither active nor archived). If a matching archived Tag Group exists, it is handled by step 5 (unarchive) instead.
**Example Pseudo:**
```pseudo theme={null}
if matchedTagGroup is null:
newTagGroup.name = dimension.name
newTagGroup.code = dimension.code
newTagGroup.archived = false
POST newTagGroup to Pleo
```
#### Example Request
```bash theme={null}
curl -X POST "https://external.staging.pleo.io/v0/tag-groups?company_id=12abc3d4-e567-890e-1234-abc56e78fabc" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"name": "Finance",
"code": "FIN",
"archived": false
}'
```
```bash theme={null}
curl --request POST \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v0/tag-groups?company_id=12abc3d4-e567-890e-1234-abc56e78fabc" \
-d '{
"name": "Finance",
"code": "FIN",
"archived": false
}' \
| jq
```
#### Example Response
```json theme={null}
{
"data": {
"id": "18741f60-1ded-4467-97e8-4b0e1178b3f2",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "Finance",
"code": "FIN",
"archived": false,
"createdAt": "2026-05-21T13:52:03.646775474Z",
"updatedAt": "2026-05-21T13:52:03.646775474Z",
"metadata": {}
}
}
```
#### What it looks like in Pleo Web App
#### Example Response `BAD_REQUEST`
You'll get an error similar to below if you try to create more than 5 active Tag Groups in Pleo.
```json theme={null}
{
"error": {
"type": "BAD_REQUEST",
"message": "Active tag groups are limited to 5/company"
}
}
```
***
### 5. Unarchive or Update Existing Tag Groups
**API Endpoint**: PUT [`/v0/tag-groups/{groupId}`](/reference/tags-api/tag-groups/updates-the-tag-group-resource-by-id)
**Example parameters:** groupId: `faf59f4a-073e-4f23-9725-944d993e6310`
If a matching Tag Group is found:
* **AS Dimension is active, Pleo Tag Group is archived** → Unarchive the Tag Group by setting `archived: false`
* **AS Dimension is active, Pleo Tag Group name differs** → Update the Tag Group name to match the AS
* **AS Dimension is active, Pleo Tag Group name matches** → No action required
**Example Pseudo:**
```pseudo theme={null}
if matchedTagGroup.archived == true:
matchedTagGroup.archived = false
matchedTagGroup.name = dimension.name
PUT matchedTagGroup to Pleo
else if matchedTagGroup.name != dimension.name:
matchedTagGroup.name = dimension.name
PUT matchedTagGroup to Pleo
```
#### Example Request
In the below example the "Cost Centre" Tag Group is unarchived.
```bash theme={null}
curl -X PUT "https://external.staging.pleo.io/v0/tag-groups/faf59f4a-073e-4f23-9725-944d993e6310" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"code": "Cost Centre",
"name": "Cost Centre",
"archived": false,
"metadata": {}
}'
```
```bash theme={null}
curl --request PUT \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v0/tag-groups/faf59f4a-073e-4f23-9725-944d993e6310" \
-d '{
"code": "Cost Centre",
"name": "Cost Centre",
"archived": false,
"metadata": {}
}' \
| jq
```
#### Example Response
```json theme={null}
{
"data": {
"id": "faf59f4a-073e-4f23-9725-944d993e6310",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "Cost Centre",
"code": "Cost Centre",
"archived": false,
"createdAt": "2026-05-21T13:23:03.729251Z",
"updatedAt": "2026-05-21T13:59:14.102020Z",
"metadata": {}
}
}
```
#### What it looks like in Pleo Web App
***
### 6. Archive Tag Groups with No Matching Selected Dimension
**API Endpoint**: PUT [`/v0/tag-groups/{groupId}`](/reference/tags-api/tag-groups/updates-the-tag-group-resource-by-id)
If a Pleo Tag Group is active but its corresponding AS Dimension is no longer selected, archive the Tag Group.
Do not delete Tag Groups. Archiving is non-destructive and reversible.
**Example Pseudo:**
```pseudo theme={null}
selectedCodes = selectedDimensions.map(d => d.code.toLowerCase())
for tagGroup in tagGroups where archived == false:
if tagGroup.code.toLowerCase() not in selectedCodes:
tagGroup.archived = true
PUT tagGroup to Pleo
```
#### Example Request
```bash theme={null}
curl -X PUT "https://external.staging.pleo.io/v0/tag-groups/f1cd0c96-32c5-4b85-8b93-8f6543bc6378" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"code": "Region",
"name": "Region",
"archived": true,
"metadata": {}
}'
```
```bash theme={null}
curl --request PUT \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v0/tag-groups/f1cd0c96-32c5-4b85-8b93-8f6543bc6378" \
-d '{
"code": "Region",
"name": "Region",
"archived": true,
"metadata": {}
}' \
| jq
```
* Archiving a Tag Group does not automatically archive its Tags. Tags belonging to an archived Tag Group remain in their current state.
* As this is a PUT action, include the current values for all fields; only `archived` should change.
#### Example Response
```json theme={null}
{
"data": {
"id": "f1cd0c96-32c5-4b85-8b93-8f6543bc6378",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "Region",
"code": "Region",
"archived": true,
"createdAt": "2026-05-21T14:12:44.543271Z",
"updatedAt": "2026-05-21T14:25:14.216836Z",
"metadata": {}
}
}
```
***
## Result
The table below recaps what happened to each Tag Group across all steps.
| Tag Group | Step 1 (AS Selection) | Step 2 (Pleo State) | Step 3 (Match) | Action |
| ----------- | --------------------- | ------------------- | ------------------ | ------------------- |
| Department | Selected, active | Active | Matched (active) | No action |
| Project | Selected, active | Active | Matched (active) | No action |
| Finance | Selected, active | Does not exist | No match | Created (Step 4) |
| Cost Centre | Selected, active | Archived | Matched (archived) | Unarchived (Step 5) |
| Region | — | Active | No match | Archived (Step 6) |
Pleo now reflects the selected Dimensions from the AS.
| Tag Group | Final State in Pleo | AS Selection | Aligned with AS? |
| ----------- | ------------------- | ------------ | :--------------: |
| Department | Active | Selected | ✓ Yes |
| Project | Active | Selected | ✓ Yes |
| Finance | Active | Selected | ✓ Yes |
| Cost Centre | Active | Selected | ✓ Yes |
| Region | Archived | — | ✓ Yes |
The Tag Group structure is now ready for Tag synchronisation.
***
## What Comes Next?
How to Sync Tags with Dimension Values
***
***
## Related Reading
* [Sync Tag Groups with Dimensions](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tag-groups)
* [Data Mapping](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-data-mapping)
* [Platform Capabilities — Tags Sync](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview)
***
# How to Sync Tags with Dimension Values
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/imports/tags/how-to-sync-tags-with-dimension-values
This how-to explains how an integration synchronises Tags within each Tag Group in Pleo with the Dimension Values from the Accounting System.
Syncing Tags is the third and final step in the Tags Sync workflow.
Tags in Pleo correspond to Dimension Values in the Accounting System. This step ensures bookkeepers always have accurate, current Dimension Values available when coding expenses.
Your integration must:
* Retrieve Dimension Values from the AS for each selected Dimension
* Retrieve all Tags (active and archived) from Pleo for each Tag Group
* Create, unarchive, update, or archive Tags as needed
* Resolve duplicate Tags when multiple Tags share the same code
## Prerequisites
Before you begin:
* You're familiar with the [Tags Sync Overview](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-overview) and the [Integration Design for Syncing Tags](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tags)
* Your integration is authenticated using one of the [supported authentication methods](/docs/current/integration-design/auth/integration-design-auth-overview#authentication-policy-overview)
* Your integration can call Pleo's Tags API endpoints
* [Tag Groups have been synchronised with Dimensions](/docs/current/how-tos/accounting-integrations/imports/tags/how-to-sync-tag-groups-with-accounting-dimensions)
## Scenario
This how-to continues from [How to Sync Tag Groups with Accounting Dimensions](/docs/current/how-tos/accounting-integrations/imports/tags/how-to-sync-tag-groups-with-accounting-dimensions).
The four active Tag Groups from that step (**Department**, **Project**, **Finance**, and **Cost Centre**) are the input for this sync. This how-to uses the **Department** Tag Group to illustrate each step.
The table below shows the starting state for the Department Tag Group before this sync runs.
| Tag (Pleo) | Pleo Status | Dimension Value (AS) | AS Status |
| ----------- | ----------- | -------------------- | -------------- |
| Engineering | Active | Engineering (ENG) | Active |
| Marketing | Archived | Marketing (MKT) | Active |
| Sales | Active | — | Does not exist |
| — | — | Operations (OPS) | Active |
## Steps
### 1. Iterate Over Each Selected Tag Group
For each active Tag Group in Pleo (corresponding to a selected Dimension), perform the following steps.
**Example Pseudo:**
```pseudo theme={null}
activeTagGroups = fetchTagGroupsFromPleo(includeArchived: false)
for tagGroup in activeTagGroups:
syncTagsForTagGroup(tagGroup)
```
#### Example Result
| Tag Group | Pleo Status | Process Tags? |
| ----------- | ----------- | :-----------: |
| Department | Active | ✓ Yes |
| Project | Active | ✓ Yes |
| Finance | Active | ✓ Yes |
| Cost Centre | Active | ✓ Yes |
The following steps use **Department** as the example.
***
### 2. Retrieve Dimension Values from the Accounting System
For the current Tag Group, fetch the Dimension Values from the AS for the matching Dimension.
Include all values: active, archived, blocked, expired, or deactivated. The sync logic determines what to do with each.
**Example Pseudo:**
```pseudo theme={null}
dimensionValues = fetchDimensionValuesFromAS(dimension.code)
```
#### Example Response
The response format depends on your Accounting System. The following is a representative example for the Department Dimension:
```json theme={null}
{
"dimensionValues": [
{ "code": "ENG", "name": "Engineering", "status": "active" },
{ "code": "MKT", "name": "Marketing", "status": "active" },
{ "code": "OPS", "name": "Operations", "status": "active" }
]
}
```
#### Example Result
| Dimension Value | AS Status | Include in Sync? |
| ----------------- | --------- | :--------------: |
| Engineering (ENG) | Active | ✓ Yes |
| Marketing (MKT) | Active | ✓ Yes |
| Operations (OPS) | Active | ✓ Yes |
***
### 3. Retrieve All Tags from Pleo for the Tag Group
**API Endpoint**: GET [`/v0/tag-groups/{groupId}/tags`](/reference/tags-api/tags/returns-tags-for-given-tag-group)
**Example parameters:** groupId: `768fb809-b282-4411-875a-406f8e4c5bdb`, includeArchived: `true`
Fetch both active and archived Tags for the current Tag Group. Including archived Tags allows unarchiving rather than creating duplicates.
**Example Pseudo:**
```pseudo theme={null}
tags = fetchTagsFromPleo(tagGroup.id, includeArchived: true)
```
#### Example Request
```bash theme={null}
curl -X GET "https://external.staging.pleo.io/v0/tag-groups/768fb809-b282-4411-875a-406f8e4c5bdb/tags?include_archived=true" \
-H "Authorization: Bearer "
```
```bash theme={null}
curl --request GET \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
"https://external.staging.pleo.io/v0/tag-groups/768fb809-b282-4411-875a-406f8e4c5bdb/tags?include_archived=true" \
| jq
```
#### Example Response
```json theme={null}
{
"data": [
{
"id": "92771fb4-cdd7-4679-b594-f21a005c731a",
"groupId": "768fb809-b282-4411-875a-406f8e4c5bdb",
"name": "Engineering · ENG",
"code": "ENG",
"archived": false,
"createdAt": "2026-05-22T14:12:43.358326Z",
"updatedAt": "2026-05-22T14:12:43.358326Z"
},
{
"id": "8fef04fe-0fe5-44ef-b863-a6fa93fdcfc4",
"groupId": "768fb809-b282-4411-875a-406f8e4c5bdb",
"name": "Marketing · MKT",
"code": "MKT",
"archived": true,
"createdAt": "2026-05-22T14:13:00.697506Z",
"updatedAt": "2026-05-22T14:19:53.667246Z"
},
{
"id": "7b079251-c225-46e6-9f0b-6f2e1b1cdec5",
"groupId": "768fb809-b282-4411-875a-406f8e4c5bdb",
"name": "Sales · Sales",
"code": "Sales",
"archived": false,
"createdAt": "2026-05-22T14:13:25.178728Z",
"updatedAt": "2026-05-22T14:13:25.178728Z"
}
],
"pagination": {
"hasPreviousPage": false,
"hasNextPage": false,
"currentRequestPagination": {
"sortingKeys": [],
"sortingOrder": [],
"parameters": {
"include_archived": [
"true"
],
"groupId": [
"768fb809-b282-4411-875a-406f8e4c5bdb"
]
}
},
"startCursor": "~=AAAAAADKCBSFWFK3T3YA=SJ3R7NGN25DHTNMU6INAAXDTDI",
"endCursor": "~=AAAAAADKCBSIKCVHFRAA=PMDZEUOCEVDONHYLN4XBWHG6YU",
"total": 3
}
}
```
#### Example Result
| Tag | Pleo Status |
| ----------------- | ----------- |
| Engineering (ENG) | Active |
| Marketing (MKT) | Archived |
| Sales (Sales) | Active |
#### What it looks like in Pleo Web App
***
### 4. Resolve Duplicate Tags
Before matching, detect and resolve any Tags in Pleo that share the same code.
If multiple Tags share the same code:
* Retain the Tag whose name matches the current AS Dimension Value name (case-insensitive)
* If no name match, retain one Tag chosen at random (using the most recently created Tag as a tiebreaker is a reasonable approach)
* Archive all other duplicates
**Example Pseudo:**
```pseudo theme={null}
tagsByCode = groupTagsByCode(tags)
for code, duplicates in tagsByCode where duplicates.count > 1:
dimensionValue = findDimensionValue(dimensionValues, code)
tagToKeep = findBestMatch(duplicates, dimensionValue.name)
tagsToArchive = duplicates excluding tagToKeep
for tag in tagsToArchive:
archiveTag(tag)
```
#### Example Result
No duplicate Tags detected for the Department Tag Group. No action required.
***
### 5. Match Dimension Values to Tags by Code
For each **active** Dimension Value from the AS, attempt to find a matching Tag in Pleo using **code** (case-insensitive).
**Example Pseudo:**
```pseudo theme={null}
tagsByCode = index tags by code.toLowerCase()
activeDimensionValues = filter dimensionValues where status == "active"
for dimensionValue in activeDimensionValues:
matchedTag = tagsByCode[dimensionValue.code.toLowerCase()]
if matchedTag is null:
createTag(tagGroup, dimensionValue)
else if matchedTag.archived == true:
unarchiveTag(matchedTag, dimensionValue)
else:
updateTagIfNameChanged(matchedTag, dimensionValue)
```
#### Example Result
| Dimension Value (AS) | Tag (Pleo) | Pleo Status | Action |
| -------------------- | ----------------- | -------------- | ------------------- |
| Engineering (ENG) | Engineering (ENG) | Active | No action |
| Marketing (MKT) | Marketing (MKT) | Archived | → Step 7: Unarchive |
| Operations (OPS) | — | Does not exist | → Step 6: Create |
| — | Sales (Sales) | Active | → Step 8: Archive |
***
### 6. Create Tags for New Dimension Values
**API Endpoint**: POST [`/v0/tag-groups/{groupId}/tags`](/reference/tags-api/tags/creates-a-new-tag-sub-resource-under-the-given-tag-group)
**Example parameters:** groupId: `768fb809-b282-4411-875a-406f8e4c5bdb`
If an AS Dimension Value is active but no matching Pleo Tag exists, create a new Tag within the Tag Group.
**Example Pseudo:**
```pseudo theme={null}
if matchedTag is null:
newTag.name = dimensionValue.name
newTag.code = dimensionValue.code
newTag.archived = false
POST newTag to Pleo
```
#### Example Request
```bash theme={null}
curl -X POST "https://external.staging.pleo.io/v0/tag-groups/768fb809-b282-4411-875a-406f8e4c5bdb/tags" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"name": "Operations",
"code": "OPS",
"archived": false
}'
```
```bash theme={null}
curl --request POST \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v0/tag-groups/768fb809-b282-4411-875a-406f8e4c5bdb/tags" \
-d '{
"name": "Operations",
"code": "OPS",
"archived": false
}' \
| jq
```
#### Example Response
```json theme={null}
{
"data": {
"id": "c55b216a-ffd1-426e-8eb2-fcf8b7376a81",
"groupId": "768fb809-b282-4411-875a-406f8e4c5bdb",
"name": "Operations",
"code": "OPS",
"archived": false,
"createdAt": "2026-05-22T14:27:56.239032674Z",
"updatedAt": "2026-05-22T14:27:56.239032979Z"
}
}
```
#### What it looks like in Pleo Web App
***
### 7. Unarchive or Update Existing Tags
**API Endpoint**: PUT [`/v0/tags/{tagId}`](/reference/tags-api/tags/updates-tag-by-id)
**Example parameters:** tagId: `8fef04fe-0fe5-44ef-b863-a6fa93fdcfc4`
If a matching Tag is found:
* **AS Dimension Value is active, Pleo Tag is archived** → Unarchive the Tag by setting `archived: false` and update the name if changed
* **AS Dimension Value is active, Pleo Tag name differs** → Update the Tag name to match the AS
* **AS Dimension Value is active, Pleo Tag name matches** → No action required
**Example Pseudo:**
```pseudo theme={null}
if matchedTag.archived == true:
matchedTag.archived = false
matchedTag.name = dimensionValue.name
PUT matchedTag to Pleo
else if matchedTag.name != dimensionValue.name:
matchedTag.name = dimensionValue.name
PUT matchedTag to Pleo
```
#### Example Request
```bash theme={null}
curl -X PUT "https://external.staging.pleo.io/v0/tags/8fef04fe-0fe5-44ef-b863-a6fa93fdcfc4" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"name": "Marketing",
"code": "MKT",
"archived": false
}'
```
```bash theme={null}
curl --request PUT \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v0/tags/8fef04fe-0fe5-44ef-b863-a6fa93fdcfc4" \
-d '{
"name": "Marketing",
"code": "MKT",
"archived": false
}' \
| jq
```
#### Example Response
```json theme={null}
{
"data": {
"id": "8fef04fe-0fe5-44ef-b863-a6fa93fdcfc4",
"groupId": "768fb809-b282-4411-875a-406f8e4c5bdb",
"name": "Marketing",
"code": "MKT",
"archived": false,
"createdAt": "2026-05-22T14:13:00.697506Z",
"updatedAt": "2026-05-22T14:19:53.667246Z"
}
}
```
#### What it looks like in Pleo Web App
***
### 8. Archive Tags with No Matching Active Dimension Value
**API Endpoint**: PUT [`/v0/tags/{tagId}`](/reference/tags-api/tags/updates-tag-by-id)
For any active Tag that does not match an active Dimension Value (because the Dimension Value is absent, deleted, blocked, expired, or deactivated in the AS), archive it.
Do not delete Tags. Archiving is non-destructive and reversible.
**Example Pseudo:**
```pseudo theme={null}
activeDimensionCodes = activeDimensionValues.map(dv => dv.code.toLowerCase())
for tag in tags where archived == false:
if tag.code.toLowerCase() not in activeDimensionCodes:
tag.archived = true
PUT tag to Pleo
```
#### Example Request
```bash theme={null}
curl -X PUT "https://external.staging.pleo.io/v0/tags/7b079251-c225-46e6-9f0b-6f2e1b1cdec5" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"name": "Sales",
"code": "Sales",
"archived": true
}'
```
```bash theme={null}
curl --request PUT \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v0/tags/7b079251-c225-46e6-9f0b-6f2e1b1cdec5" \
-d '{
"name": "Sales",
"code": "Sales",
"archived": true
}' \
| jq
```
Archive a Tag whenever its corresponding Dimension Value is absent, deleted, blocked, expired, or deactivated in the AS, not only when it is explicitly marked as archived.
#### Example Response
```json theme={null}
{
"data": {
"id": "7b079251-c225-46e6-9f0b-6f2e1b1cdec5",
"groupId": "768fb809-b282-4411-875a-406f8e4c5bdb",
"name": "Sales",
"code": "Sales",
"archived": true,
"createdAt": "2026-05-22T14:13:25.178728Z",
"updatedAt": "2026-05-22T14:22:19.413192Z"
}
}
```
#### What it looks like in Pleo Web App
***
## Result
The table below recaps what happened to each Tag across all steps for the Department Tag Group.
| Tag | Step 2 (AS Dimension Value) | Step 3 (Pleo State) | Step 5 (Match) | Action |
| ----------- | --------------------------- | ------------------- | ------------------ | ------------------- |
| Engineering | Active (ENG) | Active | Matched (active) | No action |
| Marketing | Active (MKT) | Archived | Matched (archived) | Unarchived (Step 7) |
| Operations | Active (OPS) | Does not exist | No match | Created (Step 6) |
| Sales | — | Active | No match | Archived (Step 8) |
Pleo now reflects the active Dimension Values from the AS for the Department Tag Group.
| Tag | Final State in Pleo | AS Dimension Value | Aligned with AS? |
| ----------- | ------------------- | ------------------ | :--------------: |
| Engineering | Active | Active | ✓ Yes |
| Marketing | Active | Active | ✓ Yes |
| Operations | Active | Active | ✓ Yes |
| Sales | Archived | — | ✓ Yes |
This same process runs for every active Tag Group (**Project**, **Finance**, and **Cost Centre**) until all Tags across all Tag Groups are aligned with the AS.
***
## What Comes Next?
Sync Periodicity and Scheduling
***
***
## Related Reading
* [Sync Tags with Dimension Values](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tags)
* [Data Mapping](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-data-mapping)
* [Sync Periodicity and Scheduling](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-periodicity)
* [Platform Capabilities — Tags Sync](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview)
***
# How to Create, Update, and Archive Tax Codes
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/imports/tax-codes/how-to-create-update-archive-tax-codes
This how-to covers the write operations for Tax Sync: creating new Tax Codes, updating or unarchiving existing Tax Codes, and archiving Tax Codes that are no longer active in the Accounting System.
Run this after [How to Fetch and Match Tax Codes](/docs/current/how-tos/accounting-integrations/imports/tax-codes/how-to-fetch-and-match-tax-codes), which covers fetching and matching tax codes from both systems.
## Prerequisites
Before you begin:
* You have completed [How to Fetch and Match Tax Codes](/docs/current/how-tos/accounting-integrations/imports/tax-codes/how-to-fetch-and-match-tax-codes) and determined what action is needed for each tax code
* Your integration is authenticated using one of the [supported authentication methods](/docs/current/integration-design/auth/integration-design-auth-overview#authentication-policy-overview)
* Your integration can call Pleo's Tax Codes API endpoints
## Scenario
This how-to continues from the [How to Fetch and Match Tax Codes](/docs/current/how-tos/accounting-integrations/imports/tax-codes/how-to-fetch-and-match-tax-codes) scenario. After matching, the following actions are required:
| AS Tax Code | Pleo Tax Code | Action |
| ---------------------- | ---------------------- | --------- |
| VAT20 - Standard (20%) | VAT20 - Standard (20%) | No action |
| VAT5 - Reduced (5%) | VAT5 - Reduced (5%) | Unarchive |
| VAT0 - Zero Rated (0%) | — | Create |
| REV - Reverse Charge | REV - Reverse Charge | Update |
| — | EXEMPT - Exempt | Archive |
## Steps
### 1. Create Tax Codes for New AS Entries
**API Endpoint**: POST [`/v0/tax-codes`](/reference/tax-codes/create-a-new-tax-code)
If an AS tax code has no matching Pleo Tax Code, create a new Tax Code.
**Example Pseudo:**
```pseudo theme={null}
if matchedTaxCode is null:
newTaxCode.code = taxCode.code
newTaxCode.name = taxCode.name
newTaxCode.rate = taxCode.rate
newTaxCode.type = mapType(taxCode) // "reverse" or "inclusive"
newTaxCode.archived = false
POST newTaxCode to Pleo
```
#### Type Mapping
| AS Tax Code | Pleo `type` |
| --------------- | ----------- |
| Reverse tax | `reverse` |
| All other taxes | `inclusive` |
The Pleo Web App labels these **Standard** and **Reverse**; see [Tax Sync Data Mapping](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-data-mapping#tax-code-type-mapping) for the full naming note.
#### Example Request
```bash theme={null}
curl -X POST "https://external.staging.pleo.io/v0/tax-codes" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"code": "VAT0 - Zero Rated (0%)",
"name": "VAT0 - Zero Rated (0%)",
"rate": 0.00,
"type": "inclusive",
"archived": false
}'
```
```bash theme={null}
curl --request POST \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v0/tax-codes" \
-d '{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"code": "VAT0 - Zero Rated (0%)",
"name": "VAT0 - Zero Rated (0%)",
"rate": 0.00,
"type": "inclusive",
"archived": false
}' \
| jq
```
#### Example Response
```json theme={null}
{
"data": {
"id": "4386a696-20d3-47fe-a230-9516fc93222c",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "VAT0 - Zero Rated (0%)",
"code": "VAT0 - Zero Rated (0%)",
"type": "inclusive",
"rate": 0E-16,
"accountingIntegrationSystem": "pleo",
"archived": false,
"createdAt": "2026-07-07T10:11:25.715331584Z",
"updatedAt": "2026-07-07T10:11:25.715331584Z"
}
}
```
#### What it looks like in Pleo Web App
***
### 2. Unarchive or Update Existing Tax Codes
**API Endpoint**: PUT [`/v0/tax-codes/{taxCodeId}`](/reference/tax-codes/update-an-tax-code)
If a matching Tax Code is found:
* **AS tax code is active, Pleo Tax Code is archived:** Unarchive by setting `archived: false` and update name, rate, and type if they differ.
* **AS tax code is active, Pleo Tax Code details differ:** Update the Tax Code to match the AS.
* **AS tax code is active, Pleo Tax Code details match:** No action required.
**Example Pseudo:**
```pseudo theme={null}
if matchedTaxCode.archived == true:
matchedTaxCode.archived = false
matchedTaxCode.name = taxCode.name
matchedTaxCode.rate = taxCode.rate
matchedTaxCode.type = mapType(taxCode)
PUT matchedTaxCode to Pleo
else if detailsDiffer(matchedTaxCode, taxCode):
matchedTaxCode.name = taxCode.name
matchedTaxCode.rate = taxCode.rate
matchedTaxCode.type = mapType(taxCode)
PUT matchedTaxCode to Pleo
```
The `code` field represents your Accounting System's internal identifier for the tax code, and your integration should use it for matching. It should never be changed during an update. If a Pleo Tax Code matches on name or any other field but its `code` doesn't match any AS tax code, treat it as [code not found](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync#code-not-found) rather than a match: create a new Tax Code for the AS entry, and [archive the unmatched Pleo Tax Code](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync#4-archive-unmatched-tax-codes).
### Unarchive
#### Example Request
The "VAT5 - Reduced (5%)" Tax Code is unarchived in this example.
```bash theme={null}
curl -X PUT "https://external.staging.pleo.io/v0/tax-codes/c3d4e5f6-g789-012g-3456-cde78g90habc" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"code": "VAT5 - Reduced (5%)",
"name": "VAT5 - Reduced (5%)",
"rate": 0.05,
"type": "inclusive",
"archived": false
}'
```
```bash theme={null}
curl --request PUT \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v0/tax-codes/c3d4e5f6-g789-012g-3456-cde78g90habc" \
-d '{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"code": "VAT5 - Reduced (5%)",
"name": "VAT5 - Reduced (5%)",
"rate": 0.05,
"type": "inclusive",
"archived": false
}' \
| jq
```
#### Example Response
```json theme={null}
{
"data": {
"id": "c3d4e5f6-g789-012g-3456-cde78g90habc",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"code": "VAT5 - Reduced (5%)",
"name": "VAT5 - Reduced (5%)",
"rate": 0.0500000000000000,
"type": "inclusive",
"archived": false,
"createdAt": "2026-07-07T08:47:59.430364Z",
"updatedAt": "2026-07-08T10:06:51.105888Z"
}
}
```
#### What it looks like in Pleo Web App
### Update
#### Example Request
The "REV - Reverse Charge" Tax Code's rate is updated from 20% to match the AS rate of 10% in this example.
```bash theme={null}
curl -X PUT "https://external.staging.pleo.io/v0/tax-codes/b2c3d4e5-f678-901f-2345-bcd67f89gabc" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"code": "REV - Reverse Charge",
"name": "REV - Reverse Charge",
"rate": 0.10,
"type": "reverse",
"archived": false
}'
```
```bash theme={null}
curl --request PUT \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v0/tax-codes/b2c3d4e5-f678-901f-2345-bcd67f89gabc" \
-d '{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"code": "REV - Reverse Charge",
"name": "REV - Reverse Charge",
"rate": 0.10,
"type": "reverse",
"archived": false
}' \
| jq
```
#### Example Response
```json theme={null}
{
"data": {
"id": "b2c3d4e5-f678-901f-2345-bcd67f89gabc",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "REV - Reverse Charge",
"code": "REV - Reverse Charge",
"type": "reverse",
"rate": 0.1000000000000000,
"accountingIntegrationSystem": "pleo",
"archived": false,
"createdAt": "2026-07-07T08:48:52.799351Z",
"updatedAt": "2026-07-08T09:20:10.215202Z"
}
}
```
#### What it looks like in Pleo Web App
***
### 3. Archive Tax Codes with No Matching AS Entry
**API Endpoint**: PUT [`/v0/tax-codes/{taxCodeId}`](/reference/tax-codes/update-an-tax-code)
If a Pleo Tax Code is active but its corresponding AS tax code is no longer active (or does not exist), archive the Tax Code.
Do not delete Tax Codes. Archiving is non-destructive and reversible.
**Example Pseudo:**
```pseudo theme={null}
relevantASCodes = relevantASTaxCodes.map(tc => tc.code.toLowerCase())
for taxCode in pleoTaxCodes where archived == false:
if taxCode.code.toLowerCase() not in relevantASCodes:
taxCode.archived = true
PUT taxCode to Pleo
```
#### Example Request
```bash theme={null}
curl -X PUT "https://external.staging.pleo.io/v0/tax-codes/a1b2c3d4-e567-890e-1234-abc56e78fabc" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"code": "EXEMPT - Exempt",
"name": "EXEMPT - Exempt",
"rate": 0.00,
"type": "inclusive",
"archived": true
}'
```
```bash theme={null}
curl --request PUT \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v0/tax-codes/a1b2c3d4-e567-890e-1234-abc56e78fabc" \
-d '{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"code": "EXEMPT - Exempt",
"name": "EXEMPT - Exempt",
"rate": 0.00,
"type": "inclusive",
"archived": true
}' \
| jq
```
As this is a PUT action, include the current values for all fields; only `archived` should change.
#### Example Response
```json theme={null}
{
"data": {
"id": "a1b2c3d4-e567-890e-1234-abc56e78fabc",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "EXEMPT - Exempt",
"code": "EXEMPT - Exempt",
"type": "inclusive",
"rate": 0E-16,
"accountingIntegrationSystem": "pleo",
"archived": true,
"createdAt": "2026-07-07T08:49:26.679397Z",
"updatedAt": "2026-07-08T10:18:54.989629Z"
}
}
```
#### What it looks like in Pleo Web App
***
### 4. Handle Duplicate Codes in Pleo
**API Endpoint**: PUT [`/v0/tax-codes/{taxCodeId}`](/reference/tax-codes/update-an-tax-code)
If two or more Pleo Tax Codes share the same `code`, resolve the conflict before completing the sync:
* Retain the Tax Code whose rate matches the rate from the corresponding AS tax code.
* Archive the duplicate using `archived: true`.
* If all duplicates match the rate, retain one and archive the rest.
**Example Pseudo:**
```pseudo theme={null}
for each group of pleoTaxCodes with the same code:
if group.size > 1:
asTaxCode = relevantASTaxCodes.find(tc => tc.code == group.code)
preferred = group.find(tc => tc.rate == asTaxCode.rate) ?? group.first()
for duplicate in group where duplicate.id != preferred.id:
duplicate.archived = true
PUT duplicate to Pleo
```
***
## Result
The table below recaps what happened to each Tax Code across all steps.
| Tax Code | AS Status | Pleo State Before | Action | Final Pleo State |
| ---------------------- | --------- | --------------------- | ---------- | ---------------- |
| VAT20 - Standard (20%) | Active | Active, matches | No action | Active |
| VAT5 - Reduced (5%) | Active | Archived | Unarchived | Active |
| VAT0 - Zero Rated (0%) | Active | Does not exist | Created | Active |
| REV - Reverse Charge | Active | Active, rate differed | Updated | Active |
| EXEMPT - Exempt | Not in AS | Active | Archived | Archived |
Pleo now reflects the current active, relevant tax codes from the AS.
| Tax Code | Final State in Pleo | In AS? | Aligned? |
| ---------------------- | ------------------- | ------ | :------: |
| VAT20 - Standard (20%) | Active | Yes | ✓ Yes |
| VAT5 - Reduced (5%) | Active | Yes | ✓ Yes |
| VAT0 - Zero Rated (0%) | Active | Yes | ✓ Yes |
| REV - Reverse Charge | Active | Yes | ✓ Yes |
| EXEMPT - Exempt | Archived | No | ✓ Yes |
***
## What Comes Next?
Tax Sync Workflow Guide
***
***
## Related Reading
* [How to Fetch and Match Tax Codes](/docs/current/how-tos/accounting-integrations/imports/tax-codes/how-to-fetch-and-match-tax-codes)
* [Sync Tax Codes Integration Design](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync)
* [Tax Sync Data Mapping](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-data-mapping)
* [Platform Capabilities: Tax Sync](/docs/current/platform/accounting-integrations/imports/tax-codes/tax-sync-overview)
***
# How to Fetch and Match Tax Codes
Source: https://developers.pleo.io/docs/current/how-tos/accounting-integrations/imports/tax-codes/how-to-fetch-and-match-tax-codes
This how-to covers the fetch and match phase of Tax Sync: retrieving tax codes from the Accounting System and Pleo, then determining what action is needed for each. For the write operations, see [How to Create, Update, and Archive Tax Codes](/docs/current/how-tos/accounting-integrations/imports/tax-codes/how-to-create-update-archive-tax-codes).
Your integration must:
* Retrieve relevant active tax codes from the AS (filtering out irrelevant or inactive codes)
* Retrieve all Tax Codes (active and archived) from Pleo
* Match AS tax codes to Pleo Tax Codes by `code` to determine what action is needed
## Prerequisites
Before you begin:
* You're familiar with the [Tax Sync Overview](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-overview) and the [Integration Design for Syncing Tax Codes](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync)
* Your integration is authenticated using one of the [supported authentication methods](/docs/current/integration-design/auth/integration-design-auth-overview#authentication-policy-overview)
* Your integration can call Pleo's Tax Codes API endpoints
## Scenario
This how-to uses a consistent example to illustrate each step. The table below shows the starting state in both systems before this sync runs.
| Tax Code (AS) | AS Status | Tax Code (Pleo) | Pleo Status |
| ---------------------- | --------- | ---------------------- | -------------- |
| VAT20 - Standard (20%) | Active | VAT20 - Standard (20%) | Active |
| VAT5 - Reduced (5%) | Active | VAT5 - Reduced (5%) | Archived |
| VAT0 - Zero Rated (0%) | Active | — | Does not exist |
| REV - Reverse Charge | Active | REV - Reverse Charge | Active |
| — | — | EXEMPT - Exempt | Active |
## Steps
### 1. Retrieve Relevant Active Tax Codes from the Accounting System
Fetch all active tax codes from the AS, then filter them:
* **Exclude** tax codes not relevant to expense management (for example: Sales VAT)
* **Exclude** blocked, inactive, or archived tax codes
**Example Pseudo:**
```pseudo theme={null}
allASTaxCodes = fetchActiveTaxCodesFromAS()
relevantASTaxCodes = allASTaxCodes.filter(tc =>
isRelevantForExpenses(tc) AND
NOT isBlocked(tc)
)
```
#### Example Result
| Tax Code | Rate | Type | AS Status |
| ---------------------- | ---- | --------- | --------- |
| VAT20 - Standard (20%) | 0.20 | inclusive | Active |
| VAT5 - Reduced (5%) | 0.05 | inclusive | Active |
| VAT0 - Zero Rated (0%) | 0.00 | inclusive | Active |
| REV - Reverse Charge | 0.10 | reverse | Active |
***
### 2. Retrieve All Tax Codes from Pleo
**API Endpoint**: POST [`/v0/tax-codes:search`](/reference/tax-codes/returns-a-list-of-tax-codes)
Fetch both active and archived Tax Codes from Pleo. Including archived Tax Codes allows unarchiving rather than creating duplicates.
**Example Pseudo:**
```pseudo theme={null}
pleoTaxCodes = fetchTaxCodesFromPleo()
```
#### Example Request
```bash theme={null}
curl -X POST "https://external.staging.pleo.io/v0/tax-codes:search?company_id=12abc3d4-e567-890e-1234-abc56e78fabc" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{}'
```
```bash theme={null}
curl --request POST \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
-H "Content-Type: application/json" \
"https://external.staging.pleo.io/v0/tax-codes:search?company_id=12abc3d4-e567-890e-1234-abc56e78fabc" \
-d '{}' \
| jq
```
#### Example Response
```json theme={null}
{
"data": [
{
"id": "d4e5f6g7-h890-123h-4567-def89h01iabc",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "VAT20 - Standard (20%)",
"code": "VAT20 - Standard (20%)",
"ingoingTaxAccount": "",
"outgoingTaxAccount": "",
"type": "inclusive",
"rate": 0.2000000000000000,
"accountingIntegrationSystem": "pleo",
"archived": false,
"createdAt": "2026-07-07T08:46:55.778461Z",
"updatedAt": "2026-07-07T08:47:11.345094Z"
},
{
"id": "c3d4e5f6-g789-012g-3456-cde78g90habc",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "VAT5 - Reduced (5%)",
"code": "VAT5 - Reduced (5%)",
"ingoingTaxAccount": "",
"outgoingTaxAccount": "",
"type": "inclusive",
"rate": 0.0500000000000000,
"accountingIntegrationSystem": "pleo",
"archived": true,
"createdAt": "2026-07-07T08:47:59.430364Z",
"updatedAt": "2026-07-07T08:49:41.829869Z"
},
{
"id": "b2c3d4e5-f678-901f-2345-bcd67f89gabc",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "REV - Reverse Charge",
"code": "REV - Reverse Charge",
"ingoingTaxAccount": "",
"outgoingTaxAccount": "",
"type": "reverse",
"rate": 0.2000000000000000,
"accountingIntegrationSystem": "pleo",
"archived": false,
"createdAt": "2026-07-07T08:48:52.799351Z",
"updatedAt": "2026-07-07T08:49:30.424444Z"
},
{
"id": "a1b2c3d4-e567-890e-1234-abc56e78fabc",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "EXEMPT - Exempt",
"code": "EXEMPT - Exempt",
"type": "inclusive",
"rate": 0E-16,
"accountingIntegrationSystem": "pleo",
"archived": false,
"createdAt": "2026-07-07T08:49:26.679397Z",
"updatedAt": "2026-07-07T08:49:26.679397Z"
}
],
"pagination": {
"hasPreviousPage": false,
"hasNextPage": false,
"currentRequestPagination": {
"sortingKeys": [],
"sortingOrder": [],
"parameters": {
"company_id": [
"12abc3d4-e567-890e-1234-abc56e78fabc"
]
}
},
"startCursor": "AAAAAADKCVM2UL3QIWMA=AAAAAADKJS6MIFHNAKEA=GFSNYKRZ3JDKFGUL5BP272WS74",
"endCursor": "AAAAAADKJS6ZMKD6Y2EA=AAAAAADKJS6ZMKD6Y2EA=AAH3FI7SEZGLVMLZ2AZKIWNYLA",
"total": 4
}
}
```
#### Example Result
| Tax Code (Pleo) | Pleo Status |
| ---------------------- | ----------- |
| EXEMPT - Exempt | Active |
| REV - Reverse Charge | Active |
| VAT5 - Reduced (5%) | Archived |
| VAT20 - Standard (20%) | Active |
#### What it looks like in Pleo Web App
***
### 3. Match AS Tax Codes to Pleo Tax Codes by code
For each relevant active AS tax code, attempt to find a matching Tax Code in Pleo using `code`. Matching is **case insensitive**.
**Example Pseudo:**
```pseudo theme={null}
pleoTaxCodesByCode = index pleoTaxCodes by code (case insensitive)
for taxCode in relevantASTaxCodes:
matchedTaxCode = pleoTaxCodesByCode[taxCode.code.toLowerCase()]
if matchedTaxCode is null:
createTaxCode(taxCode)
else if matchedTaxCode.archived == true:
unarchiveAndUpdate(matchedTaxCode, taxCode)
else if detailsDiffer(matchedTaxCode, taxCode):
updateTaxCode(matchedTaxCode, taxCode)
// else: no action needed
```
#### Example Result
| AS Tax Code | Pleo Tax Code | Pleo Status | Action |
| ---------------------- | ---------------------- | -------------------- | --------- |
| VAT20 - Standard (20%) | VAT20 - Standard (20%) | Active, matches | No action |
| VAT5 - Reduced (5%) | VAT5 - Reduced (5%) | Archived | Unarchive |
| VAT0 - Zero Rated (0%) | — | Does not exist | Create |
| REV - Reverse Charge | REV - Reverse Charge | Active, rate differs | Update |
| — | EXEMPT - Exempt | Active | Archive |
***
## What Comes Next?
How to Create, Update, and Archive Tax Codes
***
***
## Related Reading
* [How to Create, Update, and Archive Tax Codes](/docs/current/how-tos/accounting-integrations/imports/tax-codes/how-to-create-update-archive-tax-codes)
* [Sync Tax Codes Integration Design](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync)
* [Tax Sync Data Mapping](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-data-mapping)
* [Platform Capabilities: Tax Sync](/docs/current/platform/accounting-integrations/imports/tax-codes/tax-sync-overview)
***
# How to Generate Standalone API Keys
Source: https://developers.pleo.io/docs/current/how-tos/api-keys/how-to-generate-standalone-api-keys
**Pleo Partners** must use **OAuth 2.0** as their production authentication method, [unless exempt](/docs/current/getting-started/developer-partnership-programme#authentication-requirements).
Standalone API Keys are intended for safe exploration or internal tooling in Staging or Production.
Generating a Standalone API Key allows you to explore and test Pleo APIs securely in Staging or Production environments.
## Prerequisites
* You have access to [**Staging** or **Production**](/docs/current/how-tos/environment-access/how-to-get-access-to-all-env-standalone-api-keys) environments
* You understand that Standalone API Keys are [**restricted and not available to all customers or partners**](/docs/current/authentication/standalone-api-keys-overview)
* You understand the [permission and scope](/docs/current/authentication/standalone-api-keys-overview#how-standalone-api-keys-work-conceptually) requirements for creating Standalone API Keys
* You understand [OAuth 2.0](/docs/current/authentication/oauth/oauth-overview) is the recommended method for multi-customer integrations
## Steps
### 1. Create an API Key
1. Log into your **Pleo Web App** for [Staging](https://app.staging.pleo.io/access) or [Production](https://app.pleo.io/access)
2. Click **Settings** from the main left-hand menu
3. Scroll down and click **API Keys**
4. Click **Create API Key**
5. Enter a descriptive **Name**
6. Select an appropriate **Expiration**
7. Select the entity **Access level** the key should access
8. Tick the **Permissions** [(API Scopes)](/docs/current/authentication/api-scopes) that should apply to the key
9. Click **Create API Key**
* Follow the [Integration Design](/docs/current/integration-design/auth/api-keys/integration-design-security-for-standalone-api-keys) guidance for managing and safeguarding your API Keys.
* Integrations are always **1-to-1 connections**. Even if you only use one API Key in a [multi-entity setup](/docs/current/integration-design/auth/multi-entity/integration-design-auth-multi-entity-overview), each API call must include the correct `company_id`.
* When an API Key expires, it becomes [**disabled for all integrations**](/docs/current/integration-design/auth/api-keys/integration-design-user-exp-guidelines-for-standalone-api-keys#expiry-and-failure-recovery) using that key.
### 2. Securely Store Your API Key
1. Copy the API Key to your clipboard
2. [Store the API Key securely](/docs/current/integration-design/auth/api-keys/integration-design-security-for-standalone-api-keys)
* Once you close the modal, you **cannot view the API Key again**.
* There is **no regenerate option**. If the key is lost, you must create a new one.
## Result
* You have generated an **API Key** with the correct configuration and API scope
* You have securely stored your API Key
***
## What Comes Next?
Make your first API call using your API Key
***
***
## Related Reading
* [OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide): Step-by-step instructions on how to configure OAuth 2.0 for multi-customer integrations.
***
# How to Make an API Call Using a Standalone API Key
Source: https://developers.pleo.io/docs/current/how-tos/api-keys/how-to-make-an-api-call-using-standalone-api-keys
Follow these steps to make your first Pleo API call using `curl`.
## Before You Begin
* Make sure you have generated a [Standalone API Key](/docs/current/how-tos/api-keys/how-to-generate-standalone-api-keys)
* Understand the [permission and scope](/docs/current/authentication/standalone-api-keys-overview#how-standalone-api-keys-work-conceptually) requirements for Standalone API Keys
* Use the correct [Base URL](/docs/current/authentication/api-base-urls) for the environment you're working in
Standalone API Keys authenticate your API calls and are scoped by environment, company, and permissions. Ensure you use the correct company ID for every request.
## Example 1 – GET a List of Employees
### Requirements
* Standalone API Key with `users.read` [API Scope](/docs/current/authentication/api-scopes)
### Endpoint
[`https://external.staging.pleo.io/v2/employees`](/reference/employees/search-for-employees)
### cURL Template
Use the template below and replace your API Key:
* `YOUR-API-KEY` → your generated Standalone API Key
```bash theme={null}
curl --request GET \
-u "YOUR-API-KEY:" \
-H "Accept: application/json;charset=UTF-8" \
"https://external.staging.pleo.io/v2/employees" \
| jq
```
### Execute the Request
Example with placeholders replaced:
```bash theme={null}
curl --request GET \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
"https://external.staging.pleo.io/v2/employees" \
| jq
```
### Sample Response
```json theme={null}
{
"data": [
{
"id": "abcdefgh-1234-5abc-6789-de01234fa12b",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"firstName": "Simon",
"lastName": "Schöt",
"email": "simon.scottie@pleo.dev",
"phone": "+298200000"
},
{
"id": "987abc45-ab67-5abc-d6e7-de89012fa13c",
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"firstName": "Avani",
"lastName": "Challa",
"email": "avani.challa123@pleo.dev",
"phone": "+298200000"
}
# entries omitted for brevity
],
"pagination": {
"hasPreviousPage": false,
"hasNextPage": true,
"startCursor": "72GNR65XMJGXDAWIZ4EUE3FFNQ=AAAAAADJIF7KOJHFGDAA",
"endCursor": "QYGEX3OMBNHJZHOWD74B7B5JOE=AAAAAADJIF7KOJOZKTAA",
"total": 36
}
}
```
## Example 2 – GET Details of a Specific Company
### Requirements
* Standalone API Key with `companies:read` [API Scope](/docs/current/authentication/api-scopes)
### Endpoint
[`https://external.pleo.io/v1/companies/{companyId}`](/reference/companies/search-for-a-specific-company)
### cURL Template
Use the template below and replace your API Key and Company ID:
* `YOUR-API-KEY` → your generated Standalone API Key
* `{companyId}` → the Company ID you want details for
> Tip: You can extract the `companyId` from the response in [Example 1](#example-1-–-get-a-list-of-employees)
```bash theme={null}
curl --request GET \
-u "YOUR-API-KEY:" \
-H "Accept: application/json;charset=UTF-8" \
"https://external.staging.pleo.io/v1/companies/{companyId}" \
| jq
```
### Execute the Request
Example with placeholders replaced:
```bash theme={null}
curl --request GET \
-u "pls_1ab2cd3e4f5g6h7a89b012c34de56f78_gabc90:" \
-H "Accept: application/json;charset=UTF-8" \
"https://external.staging.pleo.io/v1/companies/12abc3d4-e567-890e-1234-abc56e78fabc" \
| jq
```
### Sample Response
```json theme={null}
{
"data": {
"id": "12abc3d4-e567-890e-1234-abc56e78fabc",
"name": "MyTestCompany",
"address": {
"addressLine1": "51462 Littel Common",
"addressLine2": "Apt. 210",
"locality": "North Kris",
"region": "Mississippi",
"postalCode": "D6H 4XH",
"country": "GB"
},
"organizationId": "98a7bc6d-efgh-5a43-b21c-0a9bc876de5f",
"registrationNumber": "12345678"
}
}
```
## Result
* You’ve successfully made API requests using your **Standalone API Key**
* You’ve verified that your API Key has the correct **permissions and scope**
* You understand how to extract IDs (like `companyId`) for subsequent requests
***
## What Comes Next?
Return to the Standalone API Key Workflow Guide
***
***
## Related Reading
* [How to Make an API Call Using a Standalone API Key (Postman)](/docs/current/how-tos/api-keys/how-to-make-an-api-call-using-standalone-api-keys-postman)
* [OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide): Step-by-step instructions on how to configure OAuth 2.0 where you handle the full token lifecycle.
***
# How to Make an API Call Using a Standalone API Key (Postman)
Source: https://developers.pleo.io/docs/current/how-tos/api-keys/how-to-make-an-api-call-using-standalone-api-keys-postman
This how-to covers making an API call using Standalone API Keys and **Postman**. For the full cURL instructions, where you use the command line, see the [How to Make an API Call Using a Standalone API Key](/docs/current/how-tos/api-keys/how-to-make-an-api-call-using-standalone-api-keys) instead.
## Prerequisites
Before you begin:
* You have [generated a Standalone API Key](/docs/current/how-tos/api-keys/how-to-generate-standalone-api-keys)
## Steps
### 1. Create an HTTP Request
In your Postman collection:
1. Click **New** and then select **HTTP Request**
2. Ensure that the HTTP method corresponds to the endpoint being called, for example: `GET`
3. Enter the **Staging URL** for the Pleo API you want to test, for example: `https://external.staging.pleo.io/v2/employees`
### 2. Add Authorisation
1. Click the **Authorization** tab
2. Select **Basic Auth** as the **Auth Type**
3. Enter your **Standalone API Key** as the **Username**
4. Leave the **Password** field blank
### 3. Send the Request
1. Click the **Send** button at the top of the screen
2. If everything is configured correctly, you should receive a JSON response
## Result
After completing these steps:
* You have successfully made a Pleo API call using Standalone API Keys in Postman
***
## What Comes Next?
Go back to guide
***
***
## Related Reading
* [OAuth 2.0 Setup with Postman Workflow Guide](/docs/current/guides/oauth-workflow-guide-postman): Step-by-step instructions on how to configure OAuth 2.0 where Postman handles the full token lifecycle.
***
# How to Get Access to Pleo Environments (Standalone API Keys)
Source: https://developers.pleo.io/docs/current/how-tos/environment-access/how-to-get-access-to-all-env-standalone-api-keys
Standalone API Keys are restricted and not available to all customers or partners. For availability rules and limitations, see the [Standalone API Keys Overview](/docs/current/authentication/standalone-api-keys-overview) article.
You require access to **Staging** or **Production** before you can generate a **Standalone API Key**.
This article explains how to:
* Confirm whether you already have access
* Request or obtain access if needed
* Continue the workflow once access is available
## 1. How to Confirm Access
You have environment access if:
* You can log into the **Pleo Web App** for [Staging](https://app.staging.pleo.io/access) or [Production](https://app.pleo.io/access)
* On the main menu on the left-hand side, you see **Settings → API Keys**
* You have **Admin** or **Bookkeeper** permissions
If the **API Keys** option is not visible, and you have the required permissions, then Standalone API Key access [has not been enabled](/docs/current/authentication/standalone-api-keys-overview).
## 2. Identify Your Role
Access depends on whether you are a **Pleo Partner** or a **Pleo Customer**.
### Pleo Partners
#### New Pleo Partners
If you are onboarding as a new partner:
1. Follow the [How to Get Access to Staging (OAuth 2.0)](/docs/current/how-tos/environment-access/how-to-get-access-to-staging-oauth) article
2. During onboarding, Pleo may enable Standalone API Key access for exploration purposes
3. Once access is confirmed, continue with the [Standalone API Key Workflow Guide](/docs/current/guides/standalone-api-keys-workflow-guide#1-generate-your-standalone-api-key)
#### Existing Pleo Partners
If Standalone API Key access has already been enabled:
* Use your existing **Staging** or **Production** login
* Continue directly with the [Standalone API Key Workflow Guide](/docs/current/guides/standalone-api-keys-workflow-guide)
### Pleo Customers
#### New Pleo Customers
You can create a Pleo account via the [Pleo website](https://www.pleo.io/en), however:
* Standalone API Key access is [**not enabled by default**](/docs/current/authentication/standalone-api-keys-overview)
* Access to the [new External APIs](#faqs) is granted only in specific cases
If you believe your use case qualifies, contact Pleo using the [chat option](https://www.pleo.io/en) on the website.
#### Existing Pleo Customers
If Standalone API Key access was enabled for your organisation:
* Log into your existing Pleo environment
* Ensure you have **Admin** or **Bookkeeper** permissions
* Continue with the [Standalone API Key Workflow Guide](/docs/current/guides/standalone-api-keys-workflow-guide#1-generate-your-standalone-api-key)
***
## What Comes Next?
Generate a Standalone API Key
***
***
## FAQs
**Legacy APIs (OpenAPI)**
* **Base URL:** `https://openapi.pleo.io`
* **Authentication:** API tokens (legacy tokens)
* **Availability:** Intended for existing/legacy use cases. Access can depend on your account setup and entitlements.
* **Lifecycle:** Deprecated. Pleo is working on a deprecation timeline and migration plan.
**New APIs (External API)**
* **Base URL:** `https://external.pleo.io`
* **Authentication:** API keys (and other authentication methods supported by the platform, including [OAuth 2.0](/docs/current/authentication/oauth/oauth-overview), depending on the use case)
* **Availability:** Restricted access. [Documentation](/reference/api-introduction) may be publicly visible, but access is not broadly enabled by default. Standalone API keys require explicit enablement.
* **What to expect:** Newer platform surface and improved structure, but not guaranteed feature parity with legacy APIs yet.
**Important:**
* Legacy API tokens **will not work** on `external.pleo.io`
* New API keys **will not work** on `openapi.pleo.io`
For more context on Standalone API Keys and environment restrictions, see the [Standalone API Keys Overview](/docs/current/authentication/standalone-api-keys-overview) article.
***
# How to Get Access to Staging (OAuth 2.0)
Source: https://developers.pleo.io/docs/current/how-tos/environment-access/how-to-get-access-to-staging-oauth
This guide applies **only to Pleo Partners**. All partner integrations must use **OAuth 2.0** as their authentication method.
Follow these steps to gain access to the **Pleo Staging environment** as a Pleo Partner and verify that you can log in successfully.
Staging access is a required first step before registering your integration and using Pleo’s APIs with OAuth 2.0.
## Prerequisites
Before you begin, make sure you have:
* A company or organisation intending to build an integration with Pleo
* A company email address to register with Pleo
* A secure place to store access credentials
This guide assumes:
* You are onboarding as a **Pleo Partner**
* You do not yet have Staging credentials
## Steps
### 1. Request Pleo Partner API Access (Partner Onboarding)
* You complete the [**Pleo Partner API Access Request**](https://forms.gle/ZW1Xp5jqwKWVmK9K9) form to register **your integration** intent.
* You provide details about:
* Your company or organisation
* Your intended ERP or Accounting System that you want to integrate with Pleo
* Planned API usage
### 2. Review and Approval
* Pleo reviews your partner request to ensure technical and security alignment.
### 3. Developer Partner Registration
Complete the **Developer Partner Registration** form sent by Pleo and supply your integration details based on your use case:
* [You're managing the full token lifecycle](/docs/current/how-tos/oauth/how-to-register-an-oauth-client#1-provide-registration-data) in Staging
* [You're using Postman to handle the token lifecycle](/docs/current/how-tos/oauth-postman/how-to-register-an-oauth-client-postman) in Staging
You don’t need a fully functional integration to get started.\
Pleo can register a placeholder OAuth 2.0 client so you can obtain credentials and begin testing in Staging.
### 4. Receive Staging Credentials
* Pleo creates a Staging company populated with **dummy data** for testing and development.
* Pleo provides your **Staging login credentials** securely via 1Password.
* Pleo provides your **OAuth 2.0 client credentials** (**Client ID** and **Client Secret**) via 1Password.
### 5. Verify Staging Credentials
* You retrieve and securely store your login details within **7 days** of notification.
* You log in at [https://app.staging.pleo.io/login](https://app.staging.pleo.io/login) using the credentials provided.
* You confirm that you can access the dashboard, see pre-loaded test data, and navigate the main sections of the Staging interface.
## Result
After completing these steps:
* You have access to the Pleo Staging Environment as a Pleo Partner and can explore the pre-loaded test data.
* You have **registered your OAuth 2.0 client** and have received your **Client ID** and **Client Secret**.
* You are **not yet ready for API testing**. You need to follow the rest of the OAuth 2.0 Setup Workflow Guide to make successful API calls.
The `Authorization: Bearer ` header must contain a valid OAuth 2.0 **access token** issued by Pleo.\
This is **not** the Client Secret and cannot be used until the OAuth 2.0 workflow has been completed.
## What Comes Next?
Go back to the OAuth 2.0 Access to Staging Workflow Guide
***
***
# How To Connect Pleo to a Pre-Built ERP or Accounting System
Source: https://developers.pleo.io/docs/current/how-tos/how-to-connect-pleo-to-pre-built-as-systems
This how-to explains **how to connect Pleo to an ERP or Accounting System** using [pre-built integrations](https://www.pleo.io/en/integrations).
## Steps to Connect
1. In the Pleo Web App, navigate to **Settings**.
2. Select **Accounting** from the menu.
3. Search for the ERP or accounting system you want to integrate with.
4. Click the desired ERP/Accounting System, then click **Connect**.
> Based on the selected system, the integration procedure may vary. For example, integrating with **Xero** will prompt you to log in to Xero to authorise access.
## Notes & Additional Options
Pre-built integrations are ready-to-use and managed by Pleo.
Custom integrations (Accounting Integrations) allow programmatic export of accounting-ready data via API and are suitable for unsupported or bespoke ERP/accounting systems.
* You can also create a [**custom integration**](/docs/current/getting-started/platform-overview#custom-export-driven-integrations) if your ERP or accounting system is not supported.
# How to Configure Postman for OAuth 2.0
Source: https://developers.pleo.io/docs/current/how-tos/oauth-postman/how-to-configure-postman-for-oauth
Configuring Postman with your OAuth 2.0 details allow you to test Pleo API endpoints, whilst leaving the token lifecycle management to Postman.
## Prerequisites
* You have completed Pleo's [registration process](/docs/current/how-tos/oauth-postman/how-to-register-an-oauth-client-postman)
* You have received your **Client ID** and **Client Secret** from Pleo
* You have logged into Postman and have created a collection
## Steps
### 1. Configure a new token
1. Open your Postman collection
2. Click the **Authorization** tab
3. Select **OAuth 2.0** as the **Auth Type**
4. Scroll to the **Configure New Token** section
5. Provide the following configuration:
| Parameter | Configuration |
| :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Grant type | Authorization Code (With PKCE) |
| Callback URL | The Postman redirect endpoint URIs you [registered](/docs/current/how-tos/oauth-postman/how-to-register-an-oauth-client-postman) |
| Auth URL | [`{AUTHORIZATION_SERVER_URL}/oauth/authorize`](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration#authorisation-server-urls) |
| Access Token URL | [`{AUTHORIZATION_SERVER_URL}/oauth/token`](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration#authorisation-server-urls) |
| Client ID | Client ID you received after [registration](/docs/current/how-tos/oauth-postman/how-to-register-an-oauth-client-postman) |
| Client Secret | Client Secret you received after [registration](/docs/current/how-tos/oauth-postman/how-to-register-an-oauth-client-postman) |
| Code Challenge Method | `SHA-256` |
| Code Verifier | Leave blank, or provide a valid [PKCE code](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-pkce-and-secured-patterns) verifier. |
| Scope | Enter space-delimited list of [API scopes](/docs/current/authentication/api-scopes) that you [registered](/docs/current/how-tos/oauth-postman/how-to-register-an-oauth-client-postman) |
| Client Authentication | `Send as Basic Auth Header` |
We recommend using Postman environment variables to store the authorisation server base URL and client credentials. This allows you to quickly switch between staging and production environments.
### 2. Get a new Access Token
1. Scroll down and click **Get New Access Token**.
2. A new browser window opens and redirects you to Pleo's authorisation server
3. Complete the authorisation process and grant access using your staging credentials
4. After authorisation, you are redirected back to Postman
5. Click **Use Token**
Postman can automatically obtain new access tokens when existing tokens expire. Ensure the **Auto-refresh Token** toggle is enabled.
## Result
* Postman is configured to authenticate with Pleo using OAuth 2.0
* You can make authenticated requests to Pleo's staging APIs
* Postman manages the access token lifecycle automatically
## What Comes Next?
Make your first API call using Postman
***
***
## Related Reading
* [OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)
Step-by-step instructions on how to configure OAuth 2.0 where you handle the full token lifecycle
***
# How to Make an API Call (Postman)
Source: https://developers.pleo.io/docs/current/how-tos/oauth-postman/how-to-make-an-api-call-postman
This how-to covers making an API call using OAuth 2.0 and **Postman**. For the full non-Postman setup, where you manage the entire token lifecycle, see the [OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide) instead.
## Prerequisites
Before you begin:
* You have [configured Postman](/docs/current/how-tos/oauth-postman/how-to-configure-postman-for-oauth) for OAuth 2.0
## Steps
### 1. Create an HTTP Request
In the Postman collection where you've configured OAuth 2.0:
1. Click **New** and then select **HTTP Request**
2. Ensure that the HTTP method corresponds to the endpoint being called, for example: `GET`
3. Enter the **Staging URL** for the Pleo API you want to test, for example: `https://external.staging.pleo.io/v2/employees`
Each API endpoint has both a **Staging** and a **Production** URL. You can find these in the [Pleo API Reference](https://developers.pleo.io/reference/employees/search-for-employees) sections.
### 2. Add authorisation
1. Click the **Authorization** tab
2. Select **OAuth 2.0** as the **Auth Type**
3. Select the OAuth 2.0 token you previously created from the **Token** dropdown menu
4. Postman automatically injects: `Authorization: Bearer `
### 3. Send the request
1. Click the **Send** button at the top of the screen
2. If everything is configured correctly, you should receive a JSON response
## Result
After completing these steps:
* You have successfully made a Pleo API call using OAuth 2.0 in Postman
## What Comes Next?
Go back to guide
***
***
## Related Reading
* [OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)
Step-by-step instructions on how to configure OAuth 2.0 where you handle the full token lifecycle
***
# How to Register an OAuth 2.0 Client (Postman)
Source: https://developers.pleo.io/docs/current/how-tos/oauth-postman/how-to-register-an-oauth-client-postman
Registering an OAuth 2.0 client allows Pleo to identify **Postman as the application**, apply the correct redirect and scope configuration, and issue client credentials (**Client ID** and **Client Secret**) securely.
This how-to is part of the **Staging onboarding process** and provides the **redirect URIs** needed for Postman when completing the Developer Partner Registration form.
This guide covers Postman clients where Postman manages redirects and the token lifecycle. If your team prefers to manage the token lifecycle yourself, follow the [How to Register an OAuth 2.0 Client](/docs/current/how-tos/oauth/how-to-register-an-oauth-client) article instead.
## Prerequisites
* You are in the process of onboarding to Pleo’s [Staging environment](/docs/current/how-tos/environment-access/how-to-get-access-to-staging-oauth).
* Pleo has asked you to complete the **Developer Partner Registration** form.
* This how-to provides the **redirect URIs** you need to submit in the registration form.
## Steps
### 1. Determine your Registration Data
Provide one or both Postman variants to Pleo to register your app depending on your team’s workflow preference.
| Postman Variant | Redirect URI |
| :-------------- | :------------------------------------------- |
| Web-based | `https://oauth.pstmn.io/v1/browser-callback` |
| Desktop | `https://oauth.pstmn.io/v1/callback` |
#### Example Registration
| Parameter | Value |
| ----------------------- | ---------------------------------------------------------------------------------------- |
| Client Name | Example Client |
| Client URI | [https://client.example/](https://client.example/) |
| Terms of Service | [https://client.example/legal/tos.html](https://client.example/legal/tos.html) |
| Privacy Policy | [https://client.example/legal/privacy.pdf](https://client.example/legal/privacy.pdf) |
| Contacts | [name.surname@example.io](mailto:name.surname@example.io) |
| Redirect URIs | [https://oauth.pstmn.io/v1/browser-callback](https://oauth.pstmn.io/v1/browser-callback) |
| Scopes | users:read users:write |
| PKCE Support | Supported |
| Subject Type Preference | None |
Verify all URLs and [API scopes](/docs/current/authentication/api-scopes) before submitting. Incorrect redirect URIs or missing scopes commonly cause authorisation failures.
***
### 2. Receive Client Credentials
Do **not** embed the Client Secret in frontend code or distribute it. Storing secrets on client devices is not supported by Pleo.
After registration, Pleo shares your credentials securely via 1Password:
| Credential | Description | Example Value |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| Client ID | Public identifier used to request authorisation and identify your OAuth 2.0 client. | 12a3b456-78c9-0d12-34e5-f678ab9bcd0e |
| Client Secret | Confidential secret used to authenticate your OAuth 2.0 client. Must be stored securely on a server and never exposed to end users. | 12345a6bcd789ef012abcd34ef5a6b |
## Result
After completing these steps:
* Postman is registered as an OAuth 2.0 client in Pleo’s Staging environment
* You have received a **Client ID** and **Client Secret**
* Postman is not yet configured to use these credentials
* API calls are **not possible** at this stage
## What Comes Next?
Configure Postman
***
***
## FAQs
These terms describe different parts of how OAuth 2.0 works in the Pleo platform.
The difference between an **integration**, **application**, and **OAuth 2.0 client** is explained in the [OAuth 2.0 Concepts and Terminology](/docs/current/authentication/oauth/oauth-overview#concepts-and-terminology) section.
***
## Related Reading
* [OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)
Step-by-step instructions on how to configure OAuth 2.0 where you handle the full token lifecycle
***
# How to Call Pleo APIs Using an Access Token
Source: https://developers.pleo.io/docs/current/how-tos/oauth/how-to-call-pleo-apis-using-access-tokens
This how-to explains how to authenticate requests to Pleo APIs using an OAuth 2.0 **access token** obtained during the authorisation flow.
## Overview
Once your OAuth 2.0 client has exchanged an authorisation code for tokens, the **access token** is used to authenticate requests to Pleo APIs.
At this stage:
* User consent has already been granted
* Tokens are stored securely on your backend
* Your integration can begin interacting with Pleo resources
## Prerequisites
Before you begin:
* You have completed the OAuth 2.0 authorisation flow
* You have successfully exchanged an authorisation code for tokens
* You have a valid, unexpired access token stored securely
## Steps
### 1. Include the Access Token in API Requests
All authenticated requests to Pleo APIs must include the access token in the `Authorization` header.
#### Required Header
```http theme={null}
Authorization: Bearer
```
* The token must be prefixed with Bearer
* The header must be included on every request to protected endpoints
Never include access tokens in query parameters or request bodies.
***
### 2. Ensure Required Scopes Are Granted
Access tokens are issued with one or more **scopes**, which define what your integration is allowed to do.
Before making an API call:
* Confirm the required scope was requested during authorisation
* Ensure the endpoint you are calling is covered by that scope
Requests made outside the granted scopes are rejected.
***
### 3. Make API Requests Over HTTPS
Access tokens can be used to:
* Fetch data from Pleo (for example, expenses or export jobs)
* Create or update resources
* Trigger workflows such as exports
All requests **must** be made over HTTPS.
Use a centralised API client in your integration to ensure consistent authentication, logging, and error handling.
***
### 4. Handle Authentication Errors
API requests may fail due to authentication-related issues.
Common causes include:
* Expired access token
* Invalid or revoked token
* Insufficient scopes
When an authentication error occurs:
* Do not retry the same request blindly
* Determine whether the token needs refreshing
* Restart the authorisation flow if required
***
### 5. Track Access Token Expiry
Access tokens are **short-lived**.
Your integration must:
* Track the token expiry time (`expires_in`)
* Refresh the token before or immediately after it expires
* Avoid making API calls with expired tokens
For refresh strategies and race-condition handling, see [Centralised Token Refresh](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-centralised-token-refresh)
***
### 6. Follow Security Best Practices
* Never expose access tokens to frontend clients
* Store tokens only on secure backend systems
* Encrypt tokens at rest where possible
* Log authentication failures with sufficient context for troubleshooting
* Avoid hardcoding tokens or scopes
***
## Result
After completing these steps:
* Your OAuth 2.0 client can authenticate requests using a valid access token
* API calls are authorised according to the granted scopes
* Authentication failures are handled safely and predictably
***
## What Comes Next?
Learn how to refresh access tokens securely and handle expiry
***
***
## Related Reading
* [Token Lifecycle](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview)
* [OAuth 2.0 Client Registration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-registration) – Step-by-step details of required fields, credentials, and redirect URIs.
* [OAuth 2.0 Client Configuration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration) – How to configure your client with correct endpoints, PKCE, and authentication methods.
* [PKCE and Secured Patterns](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-pkce-and-secured-patterns) – Security requirements for public clients.
***
# How to Direct Users to the Authorisation Endpoint
Source: https://developers.pleo.io/docs/current/how-tos/oauth/how-to-direct-users-to-the-authorisation-endpoint
Follow these steps to redirect users to Pleo’s OAuth 2.0 **authorisation endpoint** so they can grant your **OAuth 2.0 client** permission to access their Pleo data.
Redirecting users is required before any access tokens can be issued. At this stage, your integration does not authenticate users or make API calls.
## Prerequisites
Before you begin:
* Your OAuth 2.0 client is registered with Pleo.
* You have a **Client ID**.
* You have at least one registered **redirect URI**.
* You do not yet have access tokens.
## Steps
### 1. Decide When to Redirect Users
Redirect users when:
* They select a **Connect to Pleo** action in your application.
* Their previous authorisation has expired or been revoked.
* A refresh token can no longer be used.
This ensures users explicitly control access to their data.
### 2. Prepare PKCE Values
Pleo requires **PKCE (Proof Key for Code Exchange)** by default.
Before redirecting the user:
1. Generate a cryptographically random **code verifier**.
2. Derive a **code challenge** using SHA-256.
3. Store the code verifier securely on your server or in the user session.
The code verifier is required later when exchanging the authorisation code for tokens.
For full PKCE and client configuration details, including supported authentication methods and server URLs, see:
* [PKCE and Secured Patterns](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-pkce-and-secured-patterns)
* [OAuth 2.0 Client Configuration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration)
| Parameter | Description | Example |
| -------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------- |
| `code_verifier` (not sent) | Random value used once per authorisation request. Stored securely until the token exchange. | `ab1C_DefG2gA3_bcdefGhAbCD-efGha45cdEFGHaB6C` |
| `code_challenge` | SHA-256–derived value sent to the authorisation endpoint | `aB1C23D4EFgH-5I6JK7lMnOpQRs8T9OuvW12_Xy3zAB` |
### 3. Build the Authorisation Request
Redirect the user’s browser to Pleo’s OAuth 2.0 authorisation endpoint using an HTTP redirect.
The request **must** include the following parameters (refer to [Client Configuration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration) for server URLs and authentication method details):
| Parameter | Description | Example |
| ----------------------- | ---------------------------------------------- | --------------------------------------------- |
| `response_type` | Must be set to `code` | `code` |
| `client_id` | OAuth 2.0 Client ID issued by Pleo | `12a3b456-78c9-0d12-93f4-f567ab8cde9f` |
| `redirect_uri` | One of your registered redirect URIs | `https://client.example/callback` |
| `scope` | Space-separated list of requested API scopes | `test:test users:read` |
| `state` | Opaque value used to protect request integrity | `a1b2c3456d78e90fab1c23456d78ef` |
| `code_challenge` | PKCE challenge derived from the code verifier | `aB1C23D4EFgH-5I6JK7lMnOpQRs8T9OuvW12_Xy3zAB` |
| `code_challenge_method` | Must be `S256` | `S256` |
Generate a **unique `state` value** for every authorisation request to protect against CSRF attacks.
**Optional Pleo-specific parameter**
| Parameter | Description | Example |
| ------------------------------- | -------------------------------------------- | ------------------------------------------------------- |
| `urn_pleo_params_oauth_subject` | Subject URN returned during session handover | `urn:pleo:company:123a4567-b89c-12d3-e456-789012345678` |
### Example Redirect
```http theme={null}
HTTP/1.1 302 Found
Location: https://auth.pleo.io/oauth/authorize
?response_type=code
&client_id=12a3b456-78c9-0d12-93f4-f567ab8cde9f
&redirect_uri=https%3A%2F%2Fclient.example%2Fcallback
&scope=test%3Atest+users%3Aread
&state=a1b2c3456d78e90fab1c23456d78ef
&code_challenge=aB1C23D4EFgH-5I6JK7lMnOpQRs8T9OuvW12_Xy3zAB
&code_challenge_method=S256
```
## 4. Handle the User Consent Flow
After the redirect:
* Pleo authenticates the user (if required).
* Pleo displays a consent screen showing:
* Your application name and logo
* The requested scopes
* The user approves or denies access.
Your application must not bypass or interfere with this flow.
## 5. Receive the Redirect Response
If the user approves access:
* Pleo redirects the browser back to your `redirect_uri`.
* An **authorisation code** is included in the query parameters.
* The original `state` value is returned unchanged.
If the user denies access:
* The redirect contains an error.
* No authorisation code is issued.
### Example Redirect Response
```http theme={null}
HTTP/1.1 302 Found
Location: https://client.example/callback
?code=Abcde1FgHIJKlMN2OpQrST
&state=a1b2c3456d78e90fab1c23456d78ef
```
## Result
After completing these steps:
* The user has authenticated with Pleo.
* The user has granted or denied consent.
* Your registered OAuth 2.0 client has received an **authorisation code** if the user granted consent.
* No access tokens exist yet.
* API calls are **not possible** at this stage.
## What Comes Next?
Exchange the authorisation code for tokens
***
***
## FAQs
These terms describe different parts of how OAuth 2.0 works in the Pleo platform.
The difference between an **integration**, **application**, and **OAuth 2.0 client** is explained in the [OAuth 2.0 Concepts and Terminology](/docs/current/authentication/oauth/oauth-overview#concepts-and-terminology) section.
***
## Related Reading
* [OAuth 2.0 Client Registration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-registration) – Step-by-step details of required fields, credentials, and redirect URIs.
* [OAuth 2.0 Client Configuration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration) – How to configure your client with correct endpoints, PKCE, and authentication methods.
* [PKCE and Secured Patterns](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-pkce-and-secured-patterns) – Security requirements for public clients.
***
# How to Handle Redirects and Exchange an Authorisation Code
Source: https://developers.pleo.io/docs/current/how-tos/oauth/how-to-handle-redirects-and-exchange-authorisation-code
This how-to explains how to handle the redirect from Pleo after **user consent** and how to exchange the returned **authorisation code** for OAuth 2.0 tokens.
It assumes you are managing the **redirect handling and token lifecycle on your backend**. For context on why this is needed, see [OAuth 2.0 Client Configuration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration) and [PKCE and Secured Patterns](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-pkce-and-secured-patterns).
## Prerequisites
Before you begin, ensure that:
* You have registered an OAuth 2.0 client with Pleo
* You have implemented the authorisation redirect flow
* You have a backend endpoint registered as your `redirect_uri`
* You are using PKCE and have stored the original `code_verifier`
* You can make secure server-to-server HTTP requests
## Steps
### 1. Understand the Redirect
After a user grants consent on Pleo’s authorisation screen, Pleo redirects the user’s browser back to your application.
The redirect contains a short-lived **authorisation code** that must be exchanged for tokens before any API access is possible.
At this stage:
* User consent has already been granted
* No API access is possible yet
* Tokens must be obtained securely on the backend
***
### 2. Receive the Redirect
Pleo redirects the browser to your registered `redirect_uri` with query parameters.
A **successful** redirect includes:
* `code` — the authorisation code
* `state` — the original value sent in the authorisation request
A **failed** redirect includes:
* `error`
* `error_description` (optional)
* `state`
Your redirect endpoint must be able to handle both cases.
***
### 3. Validate the Redirect
Before exchanging the authorisation code, perform the following checks:
* **Verify the `state` value**\
Ensure it matches the value you generated before redirecting the user.\
This protects against CSRF attacks.
* **Ensure a `code` is present**\
If no authorisation code is returned, treat the flow as failed.
Never proceed with token exchange if the `state` validation fails.
***
### 4. Exchange the Authorisation Code for Tokens
The authorisation code must be exchanged **server-to-server** using Pleo’s token endpoint.
This request must **never** be made from a frontend or client-side application.
***
### 5. Send the Token Exchange Request
The token exchange request must include the following parameters:
| Parameter | Description |
| --------------- | -------------------------------------- |
| `grant_type` | Must be `authorization_code` |
| `code` | Authorisation code received from Pleo |
| `redirect_uri` | Must match the registered redirect URI |
| `client_id` | Your OAuth 2.0 Client ID |
| `client_secret` | Your OAuth 2.0 Client Secret |
| `code_verifier` | Original PKCE code verifier |
The `code_verifier` must be the exact value used to generate the `code_challenge` during authorisation.
#### Example Request
```http theme={null}
POST /oauth/token HTTP/1.1
Host: auth.pleo.io
Authorization: Basic AbCdE1F2GHIjKLMnOp34QRS5TUvwXyzaBCDeF6GhI7JkLmNOPqrsTuvwXyZaBcD8EfGhIJklMnOpQrStUVWxyZA9BC==
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=Abcde1FgHIJKlMN2OpQrST
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Fcallback
&code_verifier=ab1C_DefG2gA3_bcdefGhAbCD-efGha45cdEFGHaB6C
```
***
### 6. Handle the Token Response
A successful token exchange returns:
* `access_token`
* `refresh_token`
* `expires_in`
* `token_type`
* `scope`
Your backend must:
* Store tokens securely
* Associate tokens with the correct Pleo company or entity
* Track token expiry time for future refresh operations
For guidance on secure storage, see [Secure Token Storage](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-secure-token-storage).
***
### 7. Handle Token Exchange Errors
Token exchange can fail for several reasons:
* Invalid or expired authorisation code
* Mismatched redirect URI
* Invalid client credentials
* Incorrect or missing PKCE verifier
If the exchange fails:
* **Do not** retry with the same authorisation code
* Restart the OAuth 2.0 flow if required
* Log failures for troubleshooting
Proper error handling ensures your integration can recover gracefully and inform users if reauthentication is needed.
***
### 8. Follow Authorisation Code Rules
* Authorisation codes are **single-use** and expire quickly
* Any used or expired code will be rejected by Pleo
Always treat authorisation codes as short-lived secrets and exchange them immediately upon receipt.
***
### 9. Store Tokens Securely
Tokens must be stored according to security best practices:
* Store tokens only on secure backend systems
* Never expose tokens to end users
* Encrypt tokens at rest where possible
See [Secure Token Storage](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-secure-token-storage) for detailed guidelines and recommended patterns.
***
### 10. Track and Manage Token Expiry
To maintain uninterrupted access:
* Track the `expires_in` value for each access token
* Schedule refresh operations **before** access tokens expire
* Ensure refresh operations follow [Centralised Token Refresh](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-centralised-token-refresh) and [Race Condition Prevention](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-race-condition-prevention) patterns
***
## Result
After completing these steps:
* Your backend has securely validated the OAuth 2.0 redirect
* Your registered OAuth 2.0 client has exchanged a **single-use authorisation code** for tokens
* Access and refresh tokens are stored securely
* Your OAuth 2.0 client is now authorised to access Pleo APIs
***
## What Comes Next?
Use your access token to make authenticated requests to Pleo APIs
***
***
## FAQs
These terms describe different parts of how OAuth 2.0 works in the Pleo platform.
The difference between an **integration**, **application**, and **OAuth 2.0 client** is explained in the [OAuth 2.0 Concepts and Terminology](/docs/current/authentication/oauth/oauth-overview#concepts-and-terminology) section.
***
## Related Reading
* [Token Lifecycle](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview)
* [OAuth 2.0 Client Registration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-registration) – Step-by-step details of required fields, credentials, and redirect URIs.
* [OAuth 2.0 Client Configuration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration) – How to configure your client with correct endpoints, PKCE, and authentication methods.
* [PKCE and Secured Patterns](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-pkce-and-secured-patterns) – Security requirements for public clients.
***
# How to Handle Token Expiry or Revocation
Source: https://developers.pleo.io/docs/current/how-tos/oauth/how-to-handle-token-expiry-or-revocation
This how-to explains how your integration should respond when OAuth 2.0 tokens can no longer be refreshed, ensuring users can reauthenticate cleanly and integrations fail safely.
## Overview
OAuth 2.0 access relies on **refresh tokens** to maintain long-lived access to Pleo APIs.\
In some situations, refresh tokens become **invalid** and can no longer be used to obtain new access tokens.
Common causes include:
* Refresh token expiry
* User revoking access
* Client credential rotation
* Security or policy changes
When this occurs, the integration must **stop API calls** and **restart the authorisation flow**.
## Steps
### 1. Detect Token Expiry or Revocation
Your integration may detect token expiry or revocation in the following ways.
**During token refresh:**
* The refresh token request returns an error (for example, an expired or invalid token)
* Pleo rejects the refresh request
**During API calls:**
* API requests fail with an authentication or authorisation error
* The error indicates that the access token is no longer valid and cannot be refreshed
Always treat refresh token failures as terminal. Retrying refresh requests will not resolve revoked or expired tokens.
***
### 2. Take Immediate Action
When token expiry or revocation is detected, your integration must:
1. Stop making further API requests using the invalid token
2. Mark the connection as **unauthenticated**
3. Invalidate stored access and refresh tokens
4. Require the user to reauthenticate
At this point, the OAuth 2.0 session cannot be recovered without user involvement.
***
### 3. Restart the Authorisation Flow
To restore access, the user must complete the OAuth 2.0 flow again.
Your integration should:
* Redirect the user to Pleo’s authorisation endpoint
* Request the required scopes again
* Handle the redirect and exchange a new authorisation code
* Store the newly issued access and refresh tokens
For implementation details, see:
* [How to Direct Users to the Authorisation Endpoint](/docs/current/how-tos/oauth/how-to-direct-users-to-the-authorisation-endpoint)
* [How to Handle Redirects and Exchange Authorisation Code](/docs/current/how-tos/oauth/how-to-handle-redirects-and-exchange-authorisation-code)
***
### 4. Design the Reauthentication Experience
Reauthentication should be:
* **Explicit** — users understand that access needs to be restored
* **Non-destructive** — existing configuration remains intact
* **Predictable** — there is a clear recovery path
Avoid silently failing or repeatedly retrying invalid tokens.
***
### 5. Log and Observe Failures
Your integration should log:
* Token refresh failures
* Detection of token expiry or revocation
* Reauthentication triggers
These logs are essential for diagnosing authentication issues and supporting users effectively.
***
### 6. Align With Integration Design Requirements
This how-to describes **how** to respond when tokens expire or are revoked.
For design-level requirements, see [Handling Refresh Token Expiry or Revocation](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-refresh-token-expiry-revocation)
***
## Result
After completing these steps:
* Invalid or revoked tokens are detected reliably
* API calls stop safely when access is no longer authorised
* Users are guided through a clean reauthentication flow
* Integrations recover predictably without data loss
***
## What Comes Next?
Go back to guide
***
***
## Related Reading
* [Token Lifecycle](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview)
* [OAuth 2.0 Client Registration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-registration) – Step-by-step details of required fields, credentials, and redirect URIs.
* [OAuth 2.0 Client Configuration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration) – How to configure your client with correct endpoints, PKCE, and authentication methods.
* [PKCE and Secured Patterns](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-pkce-and-secured-patterns) – Security requirements for public clients.
***
# How to Refresh Tokens
Source: https://developers.pleo.io/docs/current/how-tos/oauth/how-to-refresh-tokens
This how-to explains how to refresh OAuth 2.0 access tokens when they expire, allowing your integration to maintain continuous access to Pleo APIs without requiring users to reauthorise.
## Overview
Access tokens issued by Pleo are **short-lived**.\
To continue calling Pleo APIs, your integration must use the **refresh token** to obtain a new access token when the current one expires.
Refreshing tokens:
* Happens without user interaction
* Must be handled server-side
* Must be safe against race conditions and retries
## Steps
### 1. Determine When to Refresh Tokens
Your integration should refresh the access token when:
* The token is **about to expire** (preferred), or
* An API call fails due to token expiration
Use the `expires_in` value returned during token exchange to track expiry.
Refreshing tokens slightly before expiry reduces the risk of failed API calls during active workflows.
***
### 2. Send a Refresh Token Request
To refresh tokens, make a request to Pleo’s token endpoint using the **refresh token grant**.
The request must include:
* `grant_type`: `refresh_token`
* `refresh_token`: the current refresh token
* `client_id`
* `client_secret`
The request must be sent from a **secure backend service** over HTTPS.
***
### 3. Handle the Token Refresh Response
A successful refresh response returns:
* A new **access token**
* A new **refresh token**
* A new `expires_in` value
Your integration must:
* Replace the stored access token
* Replace the stored refresh token
* Update the expiry timestamp
Always persist both tokens after a successful refresh. Refresh tokens are rotated and old ones may no longer be valid.
***
### 4. Centralise Token Refresh Logic
Token refresh must be implemented in a **single, centralised location** within your integration.
This ensures:
* Consistent behaviour across all API calls
* Easier monitoring and debugging
* Correct handling of retries and failures
For design requirements, see [Centralised Token Refresh](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-centralised-token-refresh).
***
### 5. Prevent Race Conditions
Multiple concurrent processes may detect token expiry at the same time.
Your integration must ensure:
* Only **one refresh request** is made at a time
* Other requests wait for the refreshed token
Common approaches include:
* Distributed locks
* In-memory mutexes
* Database-level locks
For design requirements, see [Race Condition Prevention](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-race-condition-prevention).
***
### 6. Handle Refresh Failures
Token refresh may fail due to:
* Expired refresh token
* Revoked access
* Invalid client credentials
If a refresh attempt fails:
* Do **not** retry indefinitely
* Do **not** continue making API calls
* Mark the token state as invalid
Your integration must require the user to reauthorise.
***
### 7. Log Refresh Activity
Your integration should log:
* Refresh attempts (timestamped)
* Success or failure outcomes
* Failure reasons (where available)
These logs are critical for diagnosing authentication and session issues.
***
## Result
After completing these steps:
* Your integration refreshes access tokens without user involvement
* API access continues seamlessly across token expirations
* Token refresh behaviour is predictable, safe, and observable
***
## What Comes Next?
Learn how to recover safely when refresh tokens expire or access is revoked
***
***
## Related Reading
* [Token Lifecycle](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview)
* [OAuth 2.0 Client Registration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-registration) – Step-by-step details of required fields, credentials, and redirect URIs.
* [OAuth 2.0 Client Configuration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration) – How to configure your client with correct endpoints, PKCE, and authentication methods.
* [PKCE and Secured Patterns](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-pkce-and-secured-patterns) – Security requirements for public clients.
***
# How to Register an OAuth 2.0 Client
Source: https://developers.pleo.io/docs/current/how-tos/oauth/how-to-register-an-oauth-client
Registering an OAuth 2.0 client allows Pleo to identify your application, apply the correct redirect and scope configuration, and issue client credentials (**Client ID** and **Client Secret**) securely.
This how-to covers **advanced registration**, where you manage redirects and the access-token lifecycle yourself. For a simpler setup using Postman, see the [How to Register an OAuth 2.0 Client (Postman)](/docs/current/how-tos/oauth-postman/how-to-register-an-oauth-client-postman) article instead.
## Prerequisites
Before you begin:
* You are in the process of onboarding to Pleo’s [Staging environment](/docs/current/how-tos/environment-access/how-to-get-access-to-staging-oauth).
* Pleo has asked you to complete the **Developer Partner Registration** form.
* This how-to provides the **information** you need to submit in the registration form.
## Steps
### 1. Provide Registration Data
You provide information to Pleo in the following categories.
#### Human-Readable Information
Do not use Pleo branding or claim your app is a Pleo client.
This information is shown to end users during authorisation:
| Parameter | Description |
| ---------------- | ----------------------------------------------------------- |
| Client Name | Your application’s brand name. |
| Client URI | Link to your app homepage or integration announcement page. |
| Logo | Square image (1:1 ratio) representing your application. |
| Terms of Service | Link to the legal agreement governing use of your app. |
| Privacy Policy | Link explaining how user data is collected and processed. |
| Contacts | Email addresses for people responsible for the application. |
#### Pleo-Specific Information
This information allows Pleo to integrate your application into the product:
| Parameter | Description |
| -------------- | -------------------------------------------------------- |
| Initiation URI | Where users start connecting your app to Pleo. |
| Settings URI | Where users manage your app’s settings from within Pleo. |
#### Technical Information
Required details to establish a secure OAuth 2.0 flow for **your registered OAuth 2.0 client**:
| Parameter | Description |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Redirect URIs | URL(s) where Pleo sends users after authentication. Must be HTTPS in production. |
| Scopes | API permissions your OAuth 2.0 client needs to access on behalf of the integration. |
| PKCE Support | Required for public clients to prevent authorisation-code attacks. Contact your Pleo Partner Manager for exceptions. |
| Subject Type Preference | (Optional) Default resource type your OAuth 2.0 client operates on when calling Pleo APIs. |
For local development, loopback redirect URIs (`localhost`, `127.0.0.1`, `[::1]`) may use HTTP.
Each Pleo API defines its own set of [scopes](/docs/current/authentication/api-scopes). Your registered OAuth 2.0 client can only access APIs and perform actions that correspond to the scopes you request during registration.
#### Example Registration
| Parameter | Value |
| ----------------------- | ------------------------------------------------------------------------------------ |
| Client Name | Example Client |
| Client URI | [https://client.example/](https://client.example/) |
| Terms of Service | [https://client.example/legal/tos.html](https://client.example/legal/tos.html) |
| Privacy Policy | [https://client.example/legal/privacy.pdf](https://client.example/legal/privacy.pdf) |
| Contacts | [name.surname@example.io](mailto:name.surname@example.io) |
| Redirect URIs | [https://client.example/callback](https://client.example/callback) |
| Scopes | users:read users:write |
| PKCE Support | Supported |
| Subject Type Preference | None |
Verify all URLs and scopes before submitting. Incorrect redirect URIs or missing scopes commonly cause authorisation failures.
***
### 2. Receive Client Credentials
Do **not** embed the Client Secret in frontend code or distribute it. Storing secrets on client devices is not supported by Pleo.
After registration, Pleo shares your credentials securely via 1Password:
| Credential | Description | Example Value |
| ------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| Client ID | Public identifier used to request authorisation and identify your app. | 12a3b456-78c9-0d12-34e5-f678ab9bcd0e |
| Client Secret | Confidential secret used to authenticate your app. Must be stored securely on a server and never exposed to end users. | 12345a6bcd789ef012abcd34ef5a6b |
## Result
After completing these steps:
* Your OAuth 2.0 client is registered in Pleo’s Staging environment.
* You have received a **Client ID** and **Client Secret**.
* No users are authorised.
* No access tokens exist.
* API calls are **not possible** at this stage.
## What Comes Next?
Redirect users to the authorisation endpoint
***
***
## FAQs
These terms describe different parts of how OAuth 2.0 works in the Pleo platform.
The difference between an **integration**, **application**, and **OAuth 2.0 client** is explained in the [OAuth 2.0 Concepts and Terminology](/docs/current/authentication/oauth/oauth-overview#concepts-and-terminology) section.
***
## Related Reading
* [OAuth 2.0 Client Registration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-registration) – Step-by-step details of required fields, credentials, and redirect URIs.
* [OAuth 2.0 Client Configuration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration) – How to configure your client with correct endpoints, PKCE, and authentication methods.
* [PKCE and Secured Patterns](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-pkce-and-secured-patterns) – Security requirements for public clients.
***
# How To Add Employees to Pleo
Source: https://developers.pleo.io/docs/current/how-tos/user-management/how-to-add-employees-to-pleo
Employees must be added to Pleo before they can submit expenses, receive cards, or participate in approval workflows.
This how-to article explains how to add employees directly in the Pleo application.
## Before you begin
* You must have **Administrator** access
* Your company must be active in Pleo
## Add employees manually
1. Open the **Pleo application**
2. Navigate to **People**
3. Click **Add People**
4. Enter the employee’s email address
5. Click **Continue**
## Select card and reimbursement access
Choose the appropriate access level:
* **Card only**\
For employees who use Pleo cards for all business spending
* **Card and reimbursements**\
For employees who use cards and submit out-of-pocket expenses
* **Reimbursements only**\
For employees who submit expenses without using a card
## Next steps
After adding an employee, you can:
* [Assign a user role](/docs/current/how-tos/user-management/how-to-assign-user-roles-to-employees)
* [Set spending limits](https://help.pleo.io/en/support/solutions/articles/103000272759-set-spending-limits)
* [Issue virtual or physical cards](https://help.pleo.io/en/support/solutions/folders/103000620227)
# How To Assign User Roles to Employees
Source: https://developers.pleo.io/docs/current/how-tos/user-management/how-to-assign-user-roles-to-employees
User roles define what actions an employee can perform in Pleo.
Only users with **Administrator** access can assign or modify roles.
## Assign a role to an employee
1. Open the **Pleo application**
2. Navigate to **People**
3. Select the employee
4. Open **Permissions**
5. Assign the appropriate role:
* Employee
* Expense reviewer
* Bookkeeper
* Administrator
6. Save your changes
## Role assignment considerations
* Only **Admins** can configure integrations and exports
* **Expense reviewers** control when expenses become accounting-ready
* **Bookkeepers** manage accounting configuration and exports
Assign roles carefully to ensure proper approval and export workflows.
## What comes next?
* [Invite external bookkeepers](/docs/current/how-tos/user-management/how-to-invite-external-bookkeepers)
# How To Invite External Bookkeepers
Source: https://developers.pleo.io/docs/current/how-tos/user-management/how-to-invite-external-bookkeepers
External bookkeepers can be invited to Pleo to manage accounting tasks without being employees of the organisation.
## Before you begin
* You must have **Administrator** access
* Your organisation must be on a plan that supports external bookkeepers
## Invite an external bookkeeper
1. Open the **Pleo application**
2. Navigate to **People**
3. Click **Add People**
4. Select **Invite external bookkeeper**
5. Choose the access level:
* Limited access
* Extended access
6. Enter the bookkeeper’s name and email address
7. Click **Accept & Invite**
## Important notes
* External bookkeepers cannot manage Pleo cards
* If a bookkeeper is an internal employee, assign the **Administrator** role instead
* Permissions may vary by region and subscription plan
## What comes next?
* Find out more about the [process of inviting external bookkeepers](https://help.pleo.io/en/support/solutions/articles/103000254861-invite-external-bookkeepers) and granting them Power of Attorney
# Chart of Accounts Sync Data Mapping
Source: https://developers.pleo.io/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-data-mapping
This page describes how data from the Accounting System is mapped to Pleo's Chart of Accounts API during Chart of Accounts Sync.
Data mapping ensures that Chart of Accounts entries from the AS are reflected accurately in Pleo's Accounts.
## Implementation
* [Chart of Accounts Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/accounts-sync-workflow-guide): workflow context and sequencing
* [How to Sync Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts): API usage and step-by-step instructions
## Accounts: Data Mapping
Map the following datapoints from AS accounts to Pleo Chart of Accounts API fields.
| Pleo Chart of Accounts API Field | AS Account Datapoint |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `externalId` | Unique long-lasting identifier of the account in the AS. This is the **source of truth** identifier used for matching. |
| `code` | The user-facing account number of the Chart of Accounts entry. Displayed to users to help them identify and select the correct account. |
| `name` | Name of the Chart of Accounts entry |
| `taxCodeExternalId` | The identifier in the AS for the tax code associated with the account. Stored as a reference only. Pleo **does not** automatically link this to a Tax Code, apply it to expenses, or update any category's default tax code. |
| `archived: false` | Use when creating new active Accounts or unarchiving existing Accounts |
| `archived: true` | Use when archiving Accounts that no longer have a matching active account in the AS |
## Matching Field
Accounts are matched to their AS counterparts using the **`externalId`** field on both ends.
The `externalId` is a stable, long-lasting identifier assigned by the Accounting System. It does not change when an account's name or code is updated, making it reliable for matching across sync runs.
***
## What Comes Next?
* [Chart of Accounts Sync Periodicity and Scheduling](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-periodicity)
***
## Related Reading
* [Sync Accounts](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-sync)
* [Chart of Accounts Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/accounts-sync-workflow-guide)
* [Platform Capabilities: Chart of Accounts Sync](/docs/current/platform/accounting-integrations/imports/accounts/accounts-sync-overview)
***
# Chart of Accounts Sync Overview
Source: https://developers.pleo.io/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-overview
This section describes how to implement Chart of Accounts Sync between an Accounting System and Pleo.
It maps Chart of Accounts entries from the Accounting System to the equivalent Chart of Accounts in Pleo. Synchronisation is one-way, from the Accounting System into Pleo. The integration is responsible for all sync orchestration, reconciliation, and API operations.
The pages in this section cover the detailed implementation requirements for the sync process.
Chart of Accounts Sync is the first step in Pleo's account-to-expense chain. Accounts synced here are what bookkeepers assign to expense categories; stale or missing accounts result in exports referencing incorrect account data.
For the full account-to-expense chain, see [Platform Capabilities: Chart of Accounts Sync](/docs/current/platform/accounting-integrations/imports/accounts/accounts-sync-overview).
## Chart of Accounts Sync Execution Model
Chart of Accounts Sync is a deterministic reconciliation process executed by the integration.
On each sync run, the integration:
1. Retrieves active Accounts from the Accounting System
2. Retrieves the current state of Accounts from Pleo (active and archived)
3. Reconciles Pleo to match the Accounting System state
4. Applies the required create, update, unarchive, and archive operations via the Pleo API
The goal of each sync run is to make Pleo reflect the current state of the Chart of Accounts in the Accounting System.
Each sync run must be independent, repeatable, and produce a consistent end state.
## High-Level Process
## Matching Model
Accounts are matched to their AS counterparts using the `externalId` field. For full matching rules, see [Sync Accounts](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-sync).
## Sync Process
Chart of Accounts Sync runs as a single reconciliation step on every sync cycle.
The integration fetches all active accounts from the Accounting System and all accounts (active and archived) from Pleo, then reconciles the two using the `externalId` field.
### Reconciliation Outcomes
| Condition | Result |
| ----------------------------------------------- | -------------------------------------------------- |
| Matching active Account in Pleo, details match | Preserve existing Account |
| Matching active Account in Pleo, details differ | Update Account name and code to match AS |
| Matching archived Account in Pleo | Unarchive Account and update details if needed |
| No matching Account in Pleo | Create new Account with name, code, and externalId |
After all Accounting System accounts are processed, any remaining active Accounts in Pleo that had no match are archived.
For the full reconciliation algorithm and step-by-step implementation instructions, see [Sync Accounts](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-sync).
## Execution Requirements
| Requirement | Description |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Scheduling Ownership | The integration defines sync timing and execution frequency |
| Full Reconciliation | Every sync execution reconciles against full Accounting System state |
| Non-Overlapping Execution | Sync executions must not overlap |
| API Ownership | All changes are applied through the [Pleo Chart of Accounts API](/reference/accounts/chart-of-accounts-api-overview). See the [how-to article](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts) for step-by-step implementation instructions. |
| Rate Limit Handling | Scheduling must respect the [Pleo Chart of Accounts API rate limit](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-periodicity#pleo-api-rate-limit). |
For schedule, frequency, and rate limit details, see [Chart of Accounts Sync Periodicity and Scheduling](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-periodicity).
## Processing Principles
| Principle | Description |
| ------------------------------ | -------------------------------------------------------------------------- |
| Unidirectional Sync | Data only flows from the Accounting System into Pleo |
| Idempotent Execution | Re-running the same sync with unchanged data produces the same final state |
| Deterministic Reconciliation | Identical inputs must produce identical outputs |
| Non-Destructive Updates | Accounts are archived instead of deleted |
| Consistent Identity Resolution | Matching logic must behave consistently across executions |
| Stateless Execution | Each sync run must independently reconcile against current source state |
## Expected Outcome
After implementation:
* Pleo accurately reflects the Chart of Accounts from the Accounting System
* Accounts remain stable across repeated sync runs
* Outdated accounts are archived automatically
* Bookkeepers always have current, accurate accounts available for category assignment
***
## What Comes Next?
* [Sync Accounts](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-sync)
* [Data Mapping](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-data-mapping)
* [Sync Periodicity and Scheduling](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-periodicity)
***
## Related Reading
* [Platform Capabilities: Chart of Accounts Sync Overview](/docs/current/platform/accounting-integrations/imports/accounts/accounts-sync-overview)
* [Chart of Accounts Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/accounts-sync-workflow-guide)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# Chart of Accounts Sync Periodicity and Scheduling
Source: https://developers.pleo.io/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-periodicity
This page describes when Chart of Accounts Sync runs, how it is triggered, and the constraints integrations must respect when scheduling sync operations.
## Sync Schedule
The integration is responsible for defining and owning the sync schedule. Pleo does not trigger syncs automatically.
The recommended schedule is:
* **On connect:** run an initial sync immediately after the integration is connected and authorised
* **Recurring:** run a sync every 24 hours
This ensures Pleo Accounts are populated from the outset and remain continuously up to date.
## Ad Hoc Chart of Accounts Sync (Optional)
If technically possible, integrations should support user-triggered sync.
Ad hoc sync allows users to trigger a Chart of Accounts Sync at any moment. This is useful when:
* New accounts have been added to the Chart of Accounts in the AS and the bookkeeper wants them reflected in Pleo immediately.
* A user suspects Accounts are out of date and wants to force a refresh.
Ad hoc sync follows the same reconciliation process as scheduled sync.
## Pleo API Rate Limit
The integration should target a maximum of **500 requests per minute** to avoid errors under load, even if the [Pleo API rate limit](/docs/current/authentication/api-base-urls#rate-limits) is higher.
Integrations should:
* Monitor request volume during sync cycles.
* Implement throttling or batching if account volumes are high.
* Avoid unnecessary repeated requests within a single sync cycle.
***
## What Comes Next?
* [Chart of Accounts Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/accounts-sync-workflow-guide)
***
## Related Reading
* [Sync Accounts](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-sync)
* [Platform Capabilities: Chart of Accounts Sync](/docs/current/platform/accounting-integrations/imports/accounts/accounts-sync-overview)
***
# Sync Accounts
Source: https://developers.pleo.io/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-sync
This page describes the Chart of Accounts Sync reconciliation process: synchronising active accounts from the Accounting System with Accounts in Pleo.
This step runs on every Chart of Accounts Sync cycle.
## Implementation
* [Chart of Accounts Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/accounts-sync-workflow-guide): workflow context and sequencing
* [How to Sync Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts): API usage and step-by-step instructions
For a conceptual overview of how AS accounts map to Pleo Accounts, see [Platform Capabilities: Chart of Accounts Sync](/docs/current/platform/accounting-integrations/imports/accounts/accounts-sync-overview).
## Matching Rules
* Matching is performed using the **`externalId`** field on both ends.
* The `externalId` is the long-lasting identifier of the account in the Accounting System.
* Name and code are not used for matching, only `externalId` is used.
## Sync Process
### 1. Fetch Active Accounts from the AS
Retrieve all active accounts from the Accounting System's Chart of Accounts.
Only active accounts are used for reconciliation. Inactive, blocked, or archived accounts in the AS are not synced into Pleo.
### 2. Fetch Accounts from Pleo
Retrieve both **active and archived** Accounts from Pleo for the connected company.
Including archived Accounts allows unarchiving rather than creating duplicates if an account becomes active in the AS again.
### 3. Match Accounts by externalId
For every active AS account, attempt to find a matching Account in Pleo using the `externalId` field on both ends.
#### externalId found, Account is active in Pleo, details match
The AS account is active and the matching Pleo Account is also active with the same name and code. No action required.
#### externalId found, Account is active in Pleo, details differ
The AS account is active and the matching Pleo Account is also active, but the name or code differs. **Update** the Account's name and code to match the AS.
#### externalId found, Account is archived in Pleo
The AS account is active but the matching Pleo Account is archived. **Unarchive** the Account and update its name and code if they differ from the AS.
#### externalId not found
The AS account is active but no matching Pleo Account exists. **Create a new Account** with the account's name, code, and externalId.
### 4. Archive Unmatched Accounts
Pleo Accounts that are active but have no matching active AS account must be **archived**.
This covers accounts that were previously present in the AS but have since been deactivated, blocked, or removed.
Accounts are never permanently deleted. Archiving preserves historical data and allows unarchiving if the account becomes active in the AS again.
## Default Tax Code Reference
If the Accounting System has a default tax code per account, that identifier can be stored in Pleo via the `taxCodeExternalId` field on the Account.
This is a reference only. Pleo **does not** automatically link it to a Tax Code in Pleo, apply it to expenses, or update the default tax code on any category mapped to the account. Partners can read this field back and use it to inform their own logic if needed.
***
## What Comes Next?
* [Chart of Accounts Sync Data Mapping](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-data-mapping)
***
## Related Reading
* [How to Sync Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts)
* [Chart of Accounts Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/accounts-sync-workflow-guide)
* [Platform Capabilities: Chart of Accounts Sync](/docs/current/platform/accounting-integrations/imports/accounts/accounts-sync-overview)
***
# Tags Sync Data Mapping
Source: https://developers.pleo.io/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-data-mapping
This page describes how data from the Accounting System is mapped to Pleo's Tags API during Tags Sync.
Data mapping ensures that Dimensions and Dimension Values from the AS are reflected accurately in Pleo's Tag Groups and Tags.
## Implementation
* [Tags Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tags-sync-workflow-guide): workflow context and sequencing
* [How to Sync Tag Groups with Accounting Dimensions](/docs/current/how-tos/accounting-integrations/imports/tags/how-to-sync-tag-groups-with-accounting-dimensions): API usage and step-by-step instructions
* [How to Sync Tags with Dimension Values](/docs/current/how-tos/accounting-integrations/imports/tags/how-to-sync-tags-with-dimension-values): API usage and step-by-step instructions
## Data Mapping
### Tag Groups
Map the following datapoints from AS Dimensions to Pleo Tag Groups API fields.
| Pleo Tag Groups API Field | AS Dimension Datapoint |
| ------------------------- | -------------------------------------------------------- |
| `code` | Code of the Dimension |
| `name` | Name of the Dimension |
| `archived: false` | Use when creating new active Tag Groups |
| `archived: true` | Use when archiving Tag Groups with no matching Dimension |
### Tags
Map the following datapoints from AS Dimension Values to Pleo Tags API fields.
| Pleo Tags API Field | AS Dimension Value Datapoint |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `code` | Code of the Dimension Value |
| `name` | Name of the Dimension Value |
| `archived: false` | Use when creating new active Tags or unarchiving existing Tags |
| `archived: true` | Use to archive Tags when the corresponding Dimension Value does not exist, is deleted, blocked, expired, or deactivated in the AS |
## Matching Field
Both Tag Groups and Tags are matched to their AS counterparts using the **code** field on both ends.
Matching is **case-insensitive**.
***
## What Comes Next?
* [Sync Periodicity and Scheduling](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-periodicity)
***
## Related Reading
* [Sync Tag Groups with Dimensions](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tag-groups)
* [Sync Tags with Dimension Values](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tags)
* [Tags Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tags-sync-workflow-guide)
* [Platform Capabilities: Tags Sync](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview)
***
# Dimension Selection
Source: https://developers.pleo.io/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-dimension-selection
This page describes how integrations must expose and manage the selection of **Accounting Dimensions** for synchronisation as Tag Groups in Pleo.
Dimension selection is **[Step 1 of Tags Sync](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-overview#sync-steps)**: the configuration step that determines which Dimensions are included in each Tags Sync cycle.
## Implementation
* [Tags Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tags-sync-workflow-guide#1-select-dimensions): workflow context and sequencing
* [How to Select Dimensions for Tags Sync](/docs/current/how-tos/accounting-integrations/imports/tags/how-to-select-dimensions-for-tags-sync): API usage and step-by-step instructions
## Conceptual Model
| Concept | Description |
| --------- | ------------------------------------------------------------------------- |
| Dimension | An Accounting Dimension in the AS (e.g. Department, Project, Cost Centre) |
| Tag Group | The corresponding entity in Pleo that the Dimension maps to |
| Selection | The set of Dimensions the integration is configured to sync |
Only **selected, active Dimensions** are included in Tags Sync.
## Dimension Selection Rules
* Users can select **up to 5 Dimensions** for synchronisation.
* Only Dimensions that are **currently active** in the Accounting System can be selected.
* The integration must display available Dimensions to the user and allow them to make their selection.
## Displaying Dimensions to Users
The integration must retrieve active Dimensions from the AS and present them to the user.
Two presentation options apply depending on the AS:
* **Static Dimensions** (fixed, fewer than 5): display each Dimension as a named toggle, allowing users to enable or disable individual Dimensions.
* **Custom or more than 5 Dimensions**: display up to 5 dropdown selectors, each showing the list of available active Dimensions.
The Dimension name displayed must match its name in the Accounting System.
The integration should allow users to **refresh the list** of available Dimensions, as custom Dimensions may be added or removed after the integration is connected.
## Auto-Selection on Connection
When the integration is first connected, the integration must attempt to automatically pre-select Dimensions:
1. Pull active Dimensions from the AS.
2. Pull active Tag Groups from Pleo.
3. Match each Dimension against Tag Groups by **code** (case-insensitive).
4. Auto-select any Dimension that matches an existing Tag Group.
This reduces manual setup effort for integrators and bookkeepers who have previously configured Tags in Pleo.
## Deselecting a Dimension
When a user deselects a Dimension:
* The corresponding Tag Group in Pleo is **archived**.
* Tags belonging to that Tag Group are **no longer synchronised**.
* The Tag Group and its Tags remain in Pleo in an archived state; they are not deleted.
If a Dimension is later re-selected, the Tag Group will be unarchived and synchronisation will resume in the next sync cycle.
## Inactive Dimensions
If a Dimension that was previously selected becomes **inactive** in the AS:
* The integration must automatically deselect it.
* The corresponding Tag Group is archived.
* The Dimension no longer appears in the configuration settings.
***
## What Comes Next?
* [Sync Tag Groups with Dimensions](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tag-groups)
***
## Related Reading
* [How to Select Dimensions for Tags Sync](/docs/current/how-tos/accounting-integrations/imports/tags/how-to-select-dimensions-for-tags-sync)
* [Tags Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tags-sync-workflow-guide)
* [Platform Capabilities: Tags Sync](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview)
***
# Tags Sync Overview
Source: https://developers.pleo.io/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-overview
This section describes how to implement Tags Sync between an Accounting System and Pleo.
Tags Sync maps Accounting Dimensions to Tag Groups and Dimension Values to Tags in Pleo. Synchronisation is one-way, from the Accounting System into Pleo. The integration is responsible for all sync orchestration, reconciliation, and API operations.
The pages in this section cover the detailed implementation requirements for each step of the sync process.
For a platform-level overview of Tags Sync, including system guarantees and responsibility model, see [Platform Capabilities: Tags Sync](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview).
## Tags Sync Execution Model
Tags Sync is a deterministic reconciliation process executed by the integration.
On each sync run, the integration:
1. retrieves selected Dimensions and Dimension Values from the Accounting System
2. retrieves the current state of Tag Groups and Tags from Pleo
3. reconciles Pleo to match the Accounting System state
4. applies the required create, update, unarchive, and archive operations via the Pleo API
The goal of each sync run is to make Pleo reflect the current state of the selected Accounting Dimensions and Dimension Values.
Each sync run must be independent, repeatable, and produce a consistent end state.
## High-Level Process
## Sync Steps
Tags Sync runs in three steps. Step 1 runs once on first connection to configure which Dimensions to synchronise. Steps 2 and 3 run on a recurring schedule to keep Pleo aligned with the Accounting System.
### Step 1: Select Dimensions
The integration retrieves active Dimensions from the Accounting System and selects which ones to synchronise. On first connection, Dimensions whose code matches an existing active Tag Group in Pleo are automatically pre-selected. The selection is stored in the integration's configuration and used as input to every subsequent sync cycle.
Up to 5 Dimensions may be selected at any time. If a selected Dimension becomes inactive in the Accounting System, it is automatically deselected and its corresponding Tag Group is archived in Pleo.
For the full selection rules, auto-selection logic, and deselection behaviour, see [Dimension Selection](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-dimension-selection).
### Step 2: Sync Tag Groups with Dimensions
The integration reconciles selected Accounting Dimensions with Tag Groups in Pleo.
#### Outcomes
| Condition | Result |
| ---------------------------------- | ---------------------------- |
| Matching active Tag Group exists | Preserve existing Tag Group |
| Matching archived Tag Group exists | Unarchive existing Tag Group |
| No matching Tag Group exists | Create new Tag Group |
After all selected Dimensions are processed, any remaining active Tag Groups that no longer have a matching selected Dimension are archived.
### Step 3: Sync Tags with Dimension Values
For each synchronised Dimension–Tag Group pair, the integration reconciles Dimension Values with Tags.
#### Outcomes
| Condition | Result |
| ---------------------------- | ----------------------------------------- |
| Matching active Tag exists | Preserve Tag and update name if required |
| Matching archived Tag exists | Unarchive Tag and update name if required |
| No matching Tag exists | Create new Tag |
#### Duplicate Handling
If multiple Tags share the same code:
* retain a single Tag
* archive duplicate Tags
* ensure the retained Tag has the correct current name
After all Dimension Values are processed, any remaining active Tags that no longer have a matching Dimension Value are archived.
## Matching Model
Tag Groups and Tags are each matched to their AS counterparts using the `code` field. For full matching rules, see [Sync Tag Groups with Dimensions](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tag-groups#matching-rules) and [Sync Tags with Dimension Values](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tags#matching-rules).
## Execution Requirements
The integration is responsible for all sync scheduling and execution behaviour.
| Requirement | Description |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Scheduling Ownership | The integration defines sync timing and execution frequency |
| Full Reconciliation | Every sync execution reconciles against full Accounting System state |
| Non-Overlapping Execution | Sync executions must not overlap |
| API Ownership | All changes are applied through the [Pleo Tags API](/reference/tags-overview). See the [how-to articles](/docs/current/how-tos/accounting-integrations/imports/tags/how-to-select-dimensions-for-tags-sync) for step-by-step implementation instructions. |
| Rate Limit Handling | Scheduling must respect Accounting System API rate limits. See [Pleo API rate limits](/docs/current/authentication/api-base-urls#rate-limits). |
For schedule, frequency, and rate limit details, see [Tags Sync Periodicity and Scheduling](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-periodicity).
## Processing Principles
All implementations must follow these architectural principles.
| Principle | Description |
| ------------------------------ | -------------------------------------------------------------------------- |
| Unidirectional Sync | Data only flows from the Accounting System into Pleo |
| Idempotent Execution | Re-running the same sync with unchanged data produces the same final state |
| Deterministic Reconciliation | Identical inputs must produce identical outputs |
| Non-Destructive Updates | Entities are archived instead of deleted |
| Consistent Identity Resolution | Matching logic must behave consistently across executions |
| Stateless Execution | Each sync run must independently reconcile against current source state |
## Expected Outcome
After implementation:
* Pleo accurately reflects selected Accounting Dimensions
* Tag Groups and Tags remain stable across repeated sync runs
* outdated entities are archived automatically
* duplicate entities are eliminated through reconciliation
* bookkeepers always work with current Tags aligned to the Accounting System
***
## What Comes Next?
* [Dimension Selection](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-dimension-selection)
* [Sync Tag Groups with Dimensions](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tag-groups)
* [Sync Tags with Dimension Values](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tags)
* [Data Mapping](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-data-mapping)
* [Sync Periodicity and Scheduling](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-periodicity)
Each section describes the detailed implementation requirements for that stage of the sync process.
***
## Related Reading
* [Platform Capabilities: Tags Sync Overview](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview)
* [Tags Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tags-sync-workflow-guide)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# Tags Sync Periodicity and Scheduling
Source: https://developers.pleo.io/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-periodicity
This page describes **when Tags Sync runs**, how it is triggered, and the constraints integrations must respect when scheduling sync operations.
## Sync Schedule
Tags Sync runs **automatically every 60 minutes**.
An initial sync is performed immediately after the integration is connected and authorised.
This ensures Pleo Tags are populated from the outset and remain continuously up-to-date.
## Longer Sync Interval
If there is a risk of exceeding the Accounting System's API rate limit due to large volumes of Dimensions or Dimension Values, a longer sync interval of **12-24 hours** may be used instead.
The chosen interval should balance data freshness with API usage constraints.
## Ad Hoc Tags Sync (Optional)
If technically possible, integrations should support user-triggered sync.
Ad hoc sync allows users to:
* Trigger a Tags Sync for **all selected Dimensions** at any moment.
* Trigger a Tags Sync for a **single specific Dimension** at any moment.
This is useful when:
* A bookkeeper has added new Dimension Values in the AS and wants them reflected in Pleo immediately.
* A user suspects Tags are out of date and wants to force a refresh.
Ad hoc sync follows the same process as scheduled sync.
## Pleo API Rate Limit
Although the [Pleo API rate limit](/docs/current/authentication/api-base-urls#rate-limits) is 600 requests per minute, the integration should target a maximum of **500 requests per minute** to avoid errors under load.
Integrations should:
* Monitor request volume during sync cycles.
* Implement throttling or batching if Dimension or Tag volumes are high.
* Avoid unnecessary repeated requests within a single sync cycle.
## Tags Columns
Active Tag Groups in Pleo always have two visible columns: **Name** and **Code**.
This is consistent across all integrations, regardless of the AS used.
Some legacy integrations may have applied different column names. This does not affect matching behaviour, which is always code-based.
***
## What Comes Next?
* [Tags Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tags-sync-workflow-guide)
***
## Related Reading
* [Sync Tag Groups with Dimensions](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tag-groups)
* [Sync Tags with Dimension Values](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tags)
* [Platform Capabilities: Tags Sync](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview)
***
# Sync Tag Groups with Dimensions
Source: https://developers.pleo.io/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tag-groups
This page describes **[Step 2 of Tags Sync](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-overview#sync-steps)**: synchronising selected Accounting Dimensions from the Accounting System with Tag Groups in Pleo.
This step runs at the start of every Tags Sync cycle, before Tag synchronisation.
## Implementation
* [Tags Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tags-sync-workflow-guide#2-sync-tag-groups-with-dimensions): workflow context and sequencing
* [How to Sync Tag Groups with Accounting Dimensions](/docs/current/how-tos/accounting-integrations/imports/tags/how-to-sync-tag-groups-with-accounting-dimensions): API usage and step-by-step instructions
For a conceptual overview of how AS Dimensions map to Pleo Tag Groups, see [Platform Capabilities: Tags Sync](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview).
## Matching Rules
* Matching is performed using the **code** field on both ends.
* Matching is **case-insensitive**: a Dimension with code `DEPT` matches a Tag Group with code `dept`.
* Name differences do not affect matching: only code is used for matching. Names are updated separately if they differ.
## Sync Process
### 1. Pull selected Dimensions
Retrieve the Dimensions currently selected for synchronisation.
For each selected Dimension, check whether it is still **active** in the AS:
* **Active**: proceed with synchronisation.
* **Inactive**: automatically deselect the Dimension. The corresponding Tag Group will be archived. This Dimension is not used in the steps below.
### 2. Pull Tag Groups from Pleo
Retrieve **active and archived** Tag Groups from Pleo for the connected company.
### 3. Match Dimensions to Tag Groups
For every selected active Dimension, match against Tag Groups using the Dimension's **code** (case-insensitive).
#### Code found: Tag Group is active
The AS Dimension is active and the matching Pleo Tag Group is also active. No action required.
#### Code found: Tag Group is archived
The AS Dimension is active but the matching Pleo Tag Group is archived. **Unarchive** the Tag Group.
#### Code not found
The AS Dimension is active but no matching Pleo Tag Group exists. **Create a new Tag Group** with the Dimension's name and code.
### 4. Archive unmatched Tag Groups
Pleo Tag Groups that are active but have no matching selected AS Dimension must be **archived**.
This covers Tag Groups that existed from a previously selected Dimension that has since been deselected or removed.
Tag Groups are never permanently deleted. Archiving preserves historical data and allows unarchiving if the Dimension is re-selected.
***
## What Comes Next?
* [Sync Tags with Dimension Values](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tags)
***
## Related Reading
* [How to Sync Tag Groups with Accounting Dimensions](/docs/current/how-tos/accounting-integrations/imports/tags/how-to-sync-tag-groups-with-accounting-dimensions)
* [Tags Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tags-sync-workflow-guide)
* [Platform Capabilities: Tags Sync](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview)
***
# Sync Tags with Dimension Values
Source: https://developers.pleo.io/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-sync-tags
This page describes **[Step 3 of Tags Sync](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-overview#sync-steps)**: synchronising Dimension Values from the Accounting System with Tags in Pleo, for each synced Dimension-Tag Group pair.
This step runs immediately after Tag Group synchronisation, for every active Tag Group-Dimension pair.
## Implementation
* [Tags Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tags-sync-workflow-guide#3-sync-tags-with-dimension-values): workflow context and sequencing
* [How to Sync Tags with Dimension Values](/docs/current/how-tos/accounting-integrations/imports/tags/how-to-sync-tags-with-dimension-values): API usage and step-by-step instructions
For a conceptual overview of how AS Dimension Values map to Pleo Tags, see [Platform Capabilities: Tags Sync](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview).
## Matching Rules
* Matching is performed using the **code** field on both ends.
* Matching is **case-insensitive**: a Dimension Value with code `ENG` matches a Tag with code `eng`.
* Name differences do not affect matching: only code is used for matching. Names are updated separately if they differ.
## Sync Process
### 1. Pull Dimension Values
For each synced Dimension-Tag Group pair, retrieve **active** (non-blocked, non-expired) Dimension Values from the AS.
### 2. Pull Tags from Pleo
Retrieve **active and archived** Tags for the corresponding Tag Group from Pleo.
### 3. Match Dimension Values to Tags
For every Dimension Value, match against Tags using the Dimension Value's **code** (case-insensitive).
#### Code found: Tag is active
The AS Dimension Value is active and the matching Pleo Tag is also active. **Preserve it**.
If the Tag's name differs from the Dimension Value name, **update the Tag's name** to match.
#### Code found: Tag is archived
The AS Dimension Value is active but the matching Pleo Tag is archived. **Unarchive it**.
If the Tag's name differs from the Dimension Value name, **update the Tag's name** to match.
#### Code not found
The AS Dimension Value is active but no matching Pleo Tag exists. **Create a new Tag** with the Dimension Value's code and name.
#### Duplicate codes found
Two or more Tags in Pleo share the same code.
Apply the following resolution logic:
1. Check whether any of the duplicates also match the Dimension Value **by name**.
2. If a name match exists: **retain that Tag**, archive all other duplicates.
3. If no name match exists, or multiple name matches exist: **retain one Tag** (chosen randomly), update its name to match the Dimension Value, and **archive all other duplicates**.
Duplicate Tags can arise from legacy integrations or manual edits. The sync resolves them without data loss by archiving rather than deleting.
### 4. Archive unmatched Tags
Pleo Tags that are active but have no matching active AS Dimension Value must be **archived**.
This covers Tags corresponding to Dimension Values that have been deleted, blocked, expired, or deactivated in the AS.
Tags are never permanently deleted. Archiving preserves historical tagging data and allows unarchiving if the Dimension Value is reinstated.
***
## What Comes Next?
* [Data Mapping](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-data-mapping)
***
## Related Reading
* [How to Sync Tags with Dimension Values](/docs/current/how-tos/accounting-integrations/imports/tags/how-to-sync-tags-with-dimension-values)
* [Tags Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tags-sync-workflow-guide)
* [Platform Capabilities: Tags Sync](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview)
***
# Sync Tax Codes
Source: https://developers.pleo.io/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync
This page describes the Tax Sync reconciliation process: synchronising relevant active tax codes from the Accounting System with Tax Codes in Pleo.
This step runs on every Tax Sync cycle.
## Implementation
* [Tax Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tax-sync-workflow-guide): workflow context and sequencing
* [How to Fetch and Match Tax Codes](/docs/current/how-tos/accounting-integrations/imports/tax-codes/how-to-fetch-and-match-tax-codes): API usage and step-by-step instructions
For a conceptual overview of how AS tax codes map to Pleo Tax Codes, see [Platform Capabilities: Tax Sync](/docs/current/platform/accounting-integrations/imports/tax-codes/tax-sync-overview).
## Matching Rules
* Matching is performed using the **`code`** field on both ends.
* Matching is **case insensitive**.
* Name, rate, and type are not used for matching; only `code` is used.
## Sync Process
### 1. Fetch Relevant Active Tax Codes from the AS
Retrieve all active tax codes from the Accounting System, then apply the following filters:
* **Exclude** tax codes not relevant to expense management (for example: Sales VAT)
* **Exclude** blocked, inactive, or archived tax codes
Only the filtered set is used for reconciliation. The Accounting System is the source of truth.
### 2. Fetch Tax Codes from Pleo
Retrieve both **active and archived** Tax Codes from Pleo for the connected company.
Including archived Tax Codes allows unarchiving rather than creating duplicates if a tax code becomes active in the AS again.
### 3. Match Tax Codes by code
For every filtered active AS tax code, attempt to find a matching Tax Code in Pleo using the `code` field on both ends.
Matching is **case insensitive**.
#### code found, Tax Code is active in Pleo, details match
The AS tax code is active and the matching Pleo Tax Code is also active with the same name, rate, and type. No action required.
#### code found, Tax Code is active in Pleo, details differ
The AS tax code is active and the matching Pleo Tax Code is also active, but the name, rate, or type differs. **Update** the Tax Code's name, rate, and type to match the AS.
#### code found, Tax Code is archived in Pleo
The AS tax code is active but the matching Pleo Tax Code is archived. **Unarchive** the Tax Code and update its name, rate, and type if they differ from the AS.
#### code not found
The AS tax code is active but no matching Pleo Tax Code exists. **Create a new Tax Code** with the tax code's code, name, rate, and type.
#### Duplicate codes found in Pleo
Two or more Tax Codes in Pleo share the same code.
* Retain the Tax Code whose rate matches the rate from the matching AS tax code. Archive the duplicate.
* If all duplicates match the rate, retain one and archive the rest.
### 4. Archive Unmatched Tax Codes
Pleo Tax Codes that are active but have no matching active AS tax code must be **archived**.
This covers tax codes that were previously present in the AS but have since been deactivated, blocked, or removed.
Tax Codes are never permanently deleted. Archiving preserves historical data and allows unarchiving if the tax code becomes active in the AS again.
## Tax Code Types
For type mapping rules, see [Tax Sync Data Mapping](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-data-mapping).
***
## What Comes Next?
* [Tax Sync Data Mapping](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-data-mapping)
***
## Related Reading
* [How to Fetch and Match Tax Codes](/docs/current/how-tos/accounting-integrations/imports/tax-codes/how-to-fetch-and-match-tax-codes)
* [How to Create, Update, and Archive Tax Codes](/docs/current/how-tos/accounting-integrations/imports/tax-codes/how-to-create-update-archive-tax-codes)
* [Tax Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tax-sync-workflow-guide)
* [Platform Capabilities: Tax Sync](/docs/current/platform/accounting-integrations/imports/tax-codes/tax-sync-overview)
***
# Tax Sync Data Mapping
Source: https://developers.pleo.io/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-data-mapping
This page describes how data from the Accounting System is mapped to Pleo's Tax Codes API during Tax Sync.
Data mapping ensures that tax codes from the AS are reflected accurately in Pleo's Tax Codes.
## Implementation
* [Tax Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tax-sync-workflow-guide): workflow context and sequencing
* [How to Fetch and Match Tax Codes](/docs/current/how-tos/accounting-integrations/imports/tax-codes/how-to-fetch-and-match-tax-codes): API usage and step-by-step instructions
## Tax Codes: Data Mapping
Map the following datapoints from AS tax codes to Pleo Tax Codes API fields.
| Pleo Tax Codes API Field | AS Tax Code Datapoint |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | The identifier of the tax code in the AS. Used for matching. |
| `name` | Name of the tax code |
| `rate` | Rate of the tax code as a decimal (for example: `0.20` for 20%) |
| `type: reverse` | Use only for reverse taxes |
| `type: inclusive` | Use for all other taxes |
| `archived: false` | Use when creating new active Tax Codes or unarchiving existing Tax Codes |
| `archived: true` | Use when archiving Tax Codes that no longer have a matching active tax code in the AS, or that were previously synced but have since been deleted, archived, or deactivated |
## Matching Field
Tax Codes are matched to their AS counterparts using the **`code`** field on both ends.
Matching is **case insensitive**. The `code` field is the stable identifier used to track a tax code across sync runs, even if its name or rate changes.
## Tax Code Type Mapping
| AS Tax Code | Pleo `type` |
| --------------------------------------------------------- | ----------- |
| [Reverse tax](https://blog.pleo.io/en/vat-reverse-charge) | `reverse` |
| All other taxes | `inclusive` |
Although `exclusive` appears as a valid enum value in the API spec, it is not currently supported. Attempting to create a Tax Code with `type: exclusive` returns a `400 BAD_REQUEST` error. Do not use this value.
The Pleo Web App labels these types **Standard** and **Reverse**. The API uses `inclusive` and `reverse`. `inclusive` corresponds to **Standard** in the Web App: there is no separate `standard` value.
***
## What Comes Next?
* [Tax Sync Periodicity and Scheduling](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-periodicity)
***
## Related Reading
* [Sync Tax Codes](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync)
* [Tax Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tax-sync-workflow-guide)
* [Platform Capabilities: Tax Sync](/docs/current/platform/accounting-integrations/imports/tax-codes/tax-sync-overview)
***
# Tax Sync Overview
Source: https://developers.pleo.io/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-overview
This section describes how to implement Tax Sync between an Accounting System and Pleo.
It maps tax code data from the Accounting System to the equivalent Tax Codes in Pleo. Synchronisation is one-way, from the Accounting System into Pleo. The integration is responsible for all sync orchestration, reconciliation, and API operations.
The pages in this section cover the detailed implementation requirements for the sync process.
Tax Sync ensures Pleo always has a current, accurate list of tax codes. Stale or missing tax codes result in expenses carrying incorrect tax data when exported.
For the full tax-to-expense chain, see [Platform Capabilities: Tax Sync](/docs/current/platform/accounting-integrations/imports/tax-codes/tax-sync-overview).
## Tax Sync Execution Model
Tax Sync is a deterministic reconciliation process executed by the integration.
On each sync run, the integration:
1. Retrieves relevant active Tax Codes from the Accounting System (filtering out irrelevant or inactive codes)
2. Retrieves the current state of Tax Codes from Pleo (active and archived)
3. Reconciles Pleo to match the Accounting System state
4. Applies the required create, update, unarchive, and archive operations via the Pleo Tax Codes API
The goal of each sync run is to make Pleo reflect the current active, relevant tax codes from the Accounting System.
Each sync run must be independent, repeatable, and produce a consistent end state.
## High-Level Process
## Matching Model
Tax Codes are matched to their AS counterparts using the `code` field. For full matching rules, see [Sync Tax Codes](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync).
## Sync Process
Tax Sync runs as a single reconciliation step on every sync cycle.
The integration fetches relevant active tax codes from the Accounting System and all Tax Codes (active and archived) from Pleo, then reconciles the two using the `code` field.
### Filtering
Before reconciliation, the integration must filter the tax codes retrieved from the Accounting System:
* **Exclude** tax codes not relevant to expense management (for example: Sales VAT)
* **Exclude** blocked, inactive, or archived tax codes
Only the filtered set of active, relevant tax codes is used for reconciliation.
### Reconciliation Outcomes
| Condition | Result |
| ------------------------------------------------ | --------------------------------------------------------------- |
| Matching active Tax Code in Pleo, details match | Preserve existing Tax Code |
| Matching active Tax Code in Pleo, details differ | Update Tax Code name, rate, and type to match AS |
| Matching archived Tax Code in Pleo | Unarchive Tax Code and update details if needed |
| No matching Tax Code in Pleo | Create new Tax Code with code, name, rate, and type |
| Duplicate codes in Pleo | Retain the Tax Code matching the AS rate; archive the duplicate |
After all Accounting System tax codes are processed, any remaining active Tax Codes in Pleo that had no match are archived.
For the full reconciliation algorithm and step-by-step implementation instructions, see [Sync Tax Codes](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync).
## Tax Code Types
Pleo supports two tax code types: `inclusive` and `reverse`. For full type mapping rules, see [Tax Sync Data Mapping](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-data-mapping).
## Execution Requirements
| Requirement | Description |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Scheduling Ownership | The integration defines sync timing and execution frequency |
| Full Reconciliation | Every sync execution reconciles against full Accounting System state |
| Non-Overlapping Execution | Sync executions must not overlap |
| API Ownership | All changes are applied through the [Pleo Tax Codes API](/reference/tax-codes-overview). See the [how-to article](/docs/current/how-tos/accounting-integrations/imports/tax-codes/how-to-fetch-and-match-tax-codes) for step-by-step implementation instructions. |
| Rate Limit Handling | Scheduling must respect the [Pleo Tax Codes API rate limit](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-periodicity#pleo-api-rate-limit). |
For schedule, frequency, and rate limit details, see [Tax Sync Periodicity and Scheduling](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-periodicity).
## Processing Principles
| Principle | Description |
| ------------------------------ | -------------------------------------------------------------------------- |
| Unidirectional Sync | Data only flows from the Accounting System into Pleo |
| Idempotent Execution | Re-running the same sync with unchanged data produces the same final state |
| Deterministic Reconciliation | Identical inputs must produce identical outputs |
| Non-Destructive Updates | Tax Codes are archived instead of deleted |
| Consistent Identity Resolution | Matching logic must behave consistently across executions |
| Stateless Execution | Each sync run must independently reconcile against current source state |
## Expected Outcome
After implementation:
* Pleo accurately reflects the active, relevant tax codes from the Accounting System
* Tax Codes remain stable across repeated sync runs
* Outdated tax codes are archived automatically
* Bookkeepers always have current, accurate tax codes available for expense assignment
***
## What Comes Next?
* [Sync Tax Codes](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync)
* [Tax Sync Data Mapping](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-data-mapping)
* [Tax Sync Periodicity and Scheduling](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-periodicity)
***
## Related Reading
* [Platform Capabilities: Tax Sync Overview](/docs/current/platform/accounting-integrations/imports/tax-codes/tax-sync-overview)
* [Tax Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tax-sync-workflow-guide)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# Tax Sync Periodicity and Scheduling
Source: https://developers.pleo.io/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-periodicity
This page describes when Tax Sync runs, how it is triggered, and the constraints integrations must respect when scheduling sync operations.
## Sync Schedule
The integration is responsible for defining and owning the sync schedule. Pleo does not trigger syncs automatically.
The recommended schedule is:
* **On connect:** run an initial sync immediately after the integration is connected and authorised
* **Recurring:** run a sync every 15 minutes
This ensures Pleo Tax Codes are populated from the outset and remain continuously up to date.
## Ad Hoc Tax Sync (Optional)
If technically possible, integrations should support user-triggered sync.
Ad hoc sync allows users to trigger a Tax Sync at any moment. This is useful when:
* New tax codes have been added to the Accounting System and the bookkeeper wants them reflected in Pleo immediately.
* A user suspects Tax Codes are out of date and wants to force a refresh.
Ad hoc sync follows the same reconciliation process as scheduled sync.
## Pleo API Rate Limit
The integration should target a maximum of **500 requests per minute** to avoid errors under load, even if the [Pleo API rate limit](/docs/current/authentication/api-base-urls#rate-limits) is higher.
Integrations should:
* Monitor request volume during sync cycles.
* Implement throttling if tax code volumes are high.
* Avoid unnecessary repeated requests within a single sync cycle.
***
## What Comes Next?
* [Tax Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tax-sync-workflow-guide)
***
## Related Reading
* [Sync Tax Codes](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync)
* [Platform Capabilities: Tax Sync](/docs/current/platform/accounting-integrations/imports/tax-codes/tax-sync-overview)
***
# Integration Design Catalogue
Source: https://developers.pleo.io/docs/current/integration-design/accounting-integrations/integration-design-overview
The rules and requirements for building accounting integrations with Pleo. These pages explain what your integration must do, how to handle each scenario, and what Pleo expects. Select a section to get started.
## Exports
### Level 1: Expense Items Exporting
The foundation for all integration levels. Understand how to detect and process export jobs from Pleo and post expense data to your Accounting System, including bookkeeping method resolution, accounts mapping, data mapping, and attachment handling.
The execution model, job lifecycle, and requirements for the full export workflow.
How to detect when export jobs are available in Pleo and start them correctly.
The validation rules your integration must apply before fetching and processing export items.
How to retrieve the list of export items from Pleo for a given export job.
How to fetch the full expense data for each export item, including line items and metadata.
The rules for resolving bookkeeping methods, mapping accounts and data, handling attachments, and applying posting behaviour.
How and when to update the status of individual export items during processing.
How to mark export items as processed and complete the export job in Pleo.
## Imports
### Level 2: Tags Sync
Understand how to sync Accounting Dimensions from your Accounting System into Pleo as Tags, so bookkeepers can assign the correct dimensions to expenses before export. Covers Dimension selection, Tag Group and Tag reconciliation, data mapping, and scheduling.
The execution model, reconciliation behaviour, and implementation requirements for Tags Sync.
The rules for selecting which Accounting Dimensions to sync, including auto-selection logic and limits.
The reconciliation rules for keeping Tag Groups in Pleo aligned with selected Dimensions.
The reconciliation rules for keeping Tags in Pleo aligned with Dimension Values from the AS.
How to map Accounting Dimension data from your AS to Pleo's Tags API fields.
When Tags Sync runs, how it is triggered, and the rate limit constraints to respect.
### Level 3
#### Chart of Accounts Sync
Understand how to sync the Chart of Accounts from your Accounting System into Pleo, so bookkeepers can assign the correct accounts to expense categories before export. Covers the reconciliation model, matching by externalId, create/update/archive operations, data mapping, and scheduling.
The execution model, reconciliation behaviour, and implementation requirements for Chart of Accounts Sync.
The reconciliation algorithm for creating, updating, unarchiving, and archiving Accounts in Pleo.
How to map Chart of Accounts data from your AS to Pleo's Chart of Accounts API fields.
When Chart of Accounts Sync runs, how it is triggered, and the rate limit constraints to respect.
#### Tax Sync
Understand how to sync Tax Codes from your Accounting System into Pleo, so bookkeepers can assign the correct tax codes to expenses before export. Covers the reconciliation model, matching by code, type mapping (reverse/inclusive), duplicate handling, data mapping, and scheduling.
The execution model, reconciliation behaviour, and implementation requirements for Tax Sync.
The reconciliation algorithm for creating, updating, unarchiving, archiving, and deduplicating Tax Codes in Pleo.
How to map tax code data from your AS to Pleo's Tax Codes API fields.
When Tax Sync runs, how it is triggered, and the rate limit constraints to respect.
# Integration Design for Standalone API Keys
Source: https://developers.pleo.io/docs/current/integration-design/auth/api-keys/integration-design-for-standalone-api-keys
This article describes **integration architecture and lifecycle behaviour** when building integrations that use **Standalone API Keys**.
If you are unfamiliar with Standalone API Keys, start with the [**Standalone API Keys Overview**](/docs/current/authentication/standalone-api-keys-overview), which explains availability, intended usage, and conceptual behaviour.
This document focuses on **how integrations must behave technically** when using API Keys.
## Integration Model
Standalone API Keys enforce a **1-to-1 integration model**:
```html theme={null}
One system ←→ One Pleo company
```
The same principle applies if you have a [**multi-entity**](/docs/current/integration-design/auth/multi-entity/integration-design-auth-multi-entity-overview) setup.
### Integration Model Assumptions
Your integration must assume:
* No multi-customer onboarding
* No delegated user authorisation
* No tenant switching
* No shared credentials
### Design Implication
Treat the API Key as a **company-level installation**, not a user login or session.
## Authentication Responsibilities
When using Standalone API Keys, your integration is responsible for managing authentication behaviour.
Your system must:
* Receive the API Key from the user
* Validate API connectivity
* Associate the integration with a single company
* Maintain authentication lifecycle state
Pleo does **not** manage the key's lifecycle or credential rotation for API Key integrations.
## Installation and Activation Requirements
After a user provides a valid Standalone API Key, the integration must complete an activation process to verify connectivity and establish company association.
### Required Activation Behaviour
Integrations **must**:
* Call the [**Installation endpoint**](/reference/installations/get-client-installation) after a key is provided
* Validate that authentication succeeds
* Extract the associated `company_id` from the API response
* Persist the `company_id` for future requests
* Prevent the integration from entering an active state if activation fails
The activation step confirms that the API Key is valid and that the integration is connected to the intended Pleo organisation.
### Error Handling Expectations
If activation fails, integrations should:
* Prevent the integration from entering an active state
* Display actionable error messaging
* Allow the user to replace the API Key and retry activation
## Company ID Handling
The `company_id` links a Standalone API Key to the correct Pleo organisation and is required for most API requests.
The Company ID establishes the organisational boundary of the integration and must be treated as part of the authentication context.
### Required Integration Behaviour
Integrations **must**:
* Persist the `company_id` obtained during activation
* Use the correct `company_id` in all subsequent API requests where required
* Ensure requests are always scoped to the associated company
### Validation Expectations
Integrations should prevent situations where:
* An API Key is used with an incorrect `company_id`
* Requests are sent without organisational scoping
* A replaced API Key silently connects to a different company without confirmation
Incorrect company scoping may result in rejected API calls or unintended data access boundaries.
## Permission and Scope Awareness
Standalone API Keys are created with predefined [**API scopes**](/docs/current/authentication/api-scopes).
Integrations should:
* Validate required permissions during setup
* Surface clear errors if permissions are insufficient
* Avoid assuming full API access
Design integrations to fail clearly when required scopes are missing.
## API Key Updates and Re-Authentication
Users must be able to update their Standalone API Key to maintain integration access.
Because API Keys may expire, be revoked, or rotated, integrations must support key replacement and revalidation.
### Required Integration Behaviour
Integrations **must**:
* Allow a new API Key to replace the previous credential
* Repeat the installation and activation process after a new key is provided
* Revalidate company association using the Installation endpoint
* Verify that the new API Key contains the required API scopes
* Prevent the integration from becoming active if validation fails
### Expected System Behaviour
After a key update:
1. The previous authentication state must be invalidated
2. The new API Key must be validated
3. The associated `company_id` must be confirmed or updated
4. Normal integration operation resumes only after successful validation
## Failure and Expiry Handling
API Keys may become invalid if they:
* Expire
* Are revoked
* Are replaced
* Lose permissions
Integrations should:
* Detect authentication failures
* Notify users clearly
* Request a replacement key when required
## Relationship to OAuth 2.0
Standalone API Keys shift responsibility from platform-managed authentication to **integration-managed lifecycle control**.
| Responsibility | OAuth 2.0 | Standalone API Keys |
| --------------------- | --------------- | ---------------------- |
| Token/Key lifecycle | Managed by Pleo | Managed by integration |
| User consent | Required | Not applicable |
| Credential rotation | Automatic | Manual |
| Multi-company support | Yes | No |
| Authentication type | Access tokens | Static API key |
***
## What Comes Next?
* [User Experience Guidelines for Standalone API Keys](/docs/current/integration-design/auth/api-keys/integration-design-user-exp-guidelines-for-standalone-api-keys)
***
## Related Reading
* [Standalone API Keys Overview](/docs/current/authentication/standalone-api-keys-overview)
* [Standalone API Key Workflow Guide](/docs/current/guides/standalone-api-keys-workflow-guide)
* [User Experience Guidelines for Standalone API Keys](/docs/current/integration-design/auth/api-keys/integration-design-user-exp-guidelines-for-standalone-api-keys)
* [Security and Credential Management for Standalone API Keys](/docs/current/integration-design/auth/api-keys/integration-design-security-for-standalone-api-keys)
* [OAuth 2.0 Overview](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview)
***
# Security and Credential Management for Standalone API Keys
Source: https://developers.pleo.io/docs/current/integration-design/auth/api-keys/integration-design-security-for-standalone-api-keys
This article defines **security and credential management expectations** when working with **Standalone API Keys**.
It focuses on **how integrations and developers must protect API Keys**, maintain access, and respond to key lifecycle events.
## Key Security Principles
Standalone API Keys are **static credentials** with company-level access.\
Security responsibilities fall entirely on the integration and the organisation.
### Core Principles
* Treat API Keys as sensitive secrets, like passwords
* Limit exposure to only the systems that require access
* Enforce least-privilege [scope](/docs/current/authentication/api-scopes) selection
* Rotate or revoke keys promptly if compromised
* Avoid embedding keys in client-side applications
## Secure Storage
### Required Practices
Integrations **must**:
* Store API Keys encrypted at rest
* Mask keys after initial entry; never display the full key again
* Persist only on trusted backend systems
* Protect against accidental exposure in logs, error messages, or code repositories
### Recommended Storage Options
* Managed secrets vaults (e.g., AWS Secrets Manager, HashiCorp Vault)
* Environment variables for backend processes
* Encrypted database fields on server-side systems
Keys must **never** be embedded in frontend applications, mobile apps, or client-side JavaScript.
## Transmission and Handling
* Always use **HTTPS** when sending API Keys to backend systems or Pleo endpoints
* Avoid passing API Keys via query parameters; use headers where possible
* Validate API Key format before submission to reduce errors and unnecessary logging
## Permission and Scope Management
* Only request the API scopes your integration actually needs
* Do **not** assume full access unless explicitly required
* Surface permission errors clearly and prevent partial activation
Least-privilege API Keys reduce risk in case of leakage.
## Key Rotation and Replacement
Standalone API Keys may expire, be revoked, or rotated manually.
### Recommended Workflow
1. Generate a replacement key in the Pleo Web App (if allowed)
2. Update the key securely in your integration
3. Revalidate connectivity and company association
4. Remove or invalidate the previous key from your systems
Automated reminders or monitoring for key expiry are strongly recommended.
## Compromise and Incident Response
If a key is suspected to be exposed:
* Immediately revoke or replace the key
* Notify relevant teams and users
* Confirm the integration is reconnected with a valid key
* Audit logs to determine if unauthorized requests occurred
## Logging and Monitoring
* Avoid logging the full API Key
* Only store minimal identifiers for debugging (e.g., last 4 characters)
* Monitor API responses for authentication failures
* Alert users promptly when action is required
## Summary
Standalone API Keys require careful management:
* They are static, company-level credentials
* Security and lifecycle management is **integration responsibility**
* Following encryption, masking, scope limitation, and rotation best practices ensures safe and reliable access
Proper operational security complements the **UX Guidelines** and **Integration Design** to provide a complete integration strategy.
***
## What Comes Next?
* [Standalone API Key Workflow Guide](/docs/current/guides/standalone-api-keys-workflow-guide)
***
## Related Reading
* [Standalone API Keys Overview](/docs/current/authentication/standalone-api-keys-overview)
* [User Experience Guidelines for Standalone API Keys](/docs/current/integration-design/auth/api-keys/integration-design-user-exp-guidelines-for-standalone-api-keys)
* [OAuth 2.0 Overview](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview)
***
# User Experience Guidelines for Standalone API Keys
Source: https://developers.pleo.io/docs/current/integration-design/auth/api-keys/integration-design-user-exp-guidelines-for-standalone-api-keys
This article defines **user experience (UX) expectations** for integrations that use **Standalone API Keys**.
It focuses on **how users interact with API Key authentication** inside your product.
## UX Principles
Standalone API Keys behave differently from OAuth 2.0 authentication.
Users are providing a **long-lived company credential**, not logging into a session.
Your interface should therefore prioritise:
* Clarity of ownership
* Explicit confirmation of company connection
* Safe credential handling
* Predictable recovery when authentication fails
## API Key Input Experience
Users must have a clear and secure way to provide their API Key.
### Required Behaviour
Your UI must:
* Provide a dedicated API Key input field
* Clearly label the field as a **secret credential**
* Accept secure paste input
* Mask the API Key during entry where possible
* Never display the full key after saving
### Recommended Guidance Text
Explain to users that:
* The API Key grants company-level access
* The key should be stored securely
* The key cannot be viewed again after saving
## Saving and Confirmation Behaviour
After a user submits an API Key, the system should immediately validate it.
### Expected UX Flow
1. User enters API Key
2. User clicks **Connect** or **Save**
3. System validates authentication
4. Installation/activation process runs
5. Company association is confirmed
6. Integration becomes active
Users should never be left unsure whether authentication succeeded.
## Company Confirmation
Because Standalone API Keys connect to a **single company**, users must be able to confirm the connected organisation.
### Required Behaviour
Your UI must:
* Display the associated Company ID after activation
* Clearly indicate which company is connected
* Confirm successful installation visually
### Example Confirmation Signals
* “Connected to Company ID: XXXXX”
* Success status indicator
* Connected state badge
This prevents accidental connections to the wrong organisation.
## Error Handling Experience
Authentication failures must be clearly communicated.
### Errors Should:
* Explain what failed
* Avoid technical jargon where possible
* Provide a clear recovery action
### Examples
Good:
* “The API Key appears to be invalid. Please check and try again.”
Avoid:
* Generic messages such as “Authentication failed”.
### Required Behaviour
If activation fails:
* The integration must not enter an active state
* Users must be able to retry immediately
* The API Key field must remain editable
## API Key Visibility Rules
API Keys must be treated as sensitive secrets within the interface.
### Required UX Behaviour
* Mask the API Key after saving
* Never allow full key retrieval
* Do not expose keys in logs or UI messages
* Avoid displaying partial keys unless necessary for identification
If identification is needed, show only minimal characters (for example: `••••abcd`).
## Updating API Keys
Users must be able to replace an API Key when it expires or is rotated.
### Required UX Behaviour
Your interface must:
* Provide an **Update API Key** or **Reconnect** action
* Allow entry of a replacement key
* Re-run validation automatically
* Clearly show reconnection progress
### Expected User Flow
1. User selects **Update API Key**
2. User enters new key
3. System revalidates installation
4. Company association is confirmed again
5. Integration returns to active state
## Expiry and Failure Recovery
API Keys may stop working unexpectedly.
Your UI should help users recover quickly.
### Recommended Behaviour
* Detect authentication failures automatically
* Display persistent connection warnings
* Provide a clear “Replace API Key” action
* Avoid silent failures
Example messaging:
> “Your connection to Pleo has expired. Please update your API Key to restore access.”
## Status and Connection Visibility
Users should always understand the current integration state.
### Recommended Connection States
* Connected
* Validation in progress
* Connection failed
* Action required
Avoid ambiguous states where users cannot determine whether the integration is functioning.
## Relationship to OAuth 2.0 UX
OAuth 2.0 and API Keys create different mental models for users.
| UX Aspect | OAuth 2.0 | Standalone API Keys |
| ---------------- | --------------- | ------------------- |
| User action | Login & consent | Paste credential |
| Ownership | User account | Company credential |
| Renewal | Automatic | Manual replacement |
| Failure recovery | Reconnect login | Replace key |
| Session concept | Yes | No |
Design interfaces accordingly.
***
## What Comes Next?
* [Security and Credential Management for Standalone API Keys](/docs/current/integration-design/auth/api-keys/integration-design-security-for-standalone-api-keys)
***
## Related Reading
* [Standalone API Keys Overview](/docs/current/authentication/standalone-api-keys-overview)
* [Standalone API Key Workflow Guide](/docs/current/guides/standalone-api-keys-workflow-guide)
* [OAuth 2.0 Overview](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview)
***
# Integration Design for Integrated API Keys
Source: https://developers.pleo.io/docs/current/integration-design/auth/integrated-api-keys/integration-design-for-integrated-api-keys
This article describes **integration architecture, lifecycle behaviour, and UX expectations** when building integrations that use **Integrated API Keys**.
If you are unfamiliar with Integrated API Keys, start with the [**Integrated API Keys Overview**](/docs/current/authentication/integrated-api-keys-overview), which explains availability, intended usage, and conceptual behaviour.
This document focuses on **how integrations must behave technically** and securely when using Integrated API Keys.
## Integration Model
Integrated API Keys enforce a **1-to-1 integration model**:
```text theme={null}
One system ←→ One Pleo company (per install)
```
The same principle applies if you have a [**multi-entity**](/docs/current/integration-design/auth/multi-entity/integration-design-auth-multi-entity-overview) setup.
## Integration Model Assumptions
Integrated API Keys are **per-customer (per installation)**:
* The **integration itself is multi-customer** (one integration can be installed by many customers)
* Each customer installation gets a **unique key generated by Pleo**, scoped to that installation
* Integrations must store **one key per customer tenant** and support rotation/reconnect per customer
* No delegated user authorisation within the integration context
* Key generation and permissions are controlled by Pleo
## Design Implication
Treat each Integrated API Key as a **company-level installation credential** for a **specific customer**, not a user login or session.\
Your integration must manage **key storage, validation, and rotation per customer**.
## Authentication Responsibilities
When using Integrated API Keys, your integration is responsible for:
* Receiving the key from the customer (generated by Pleo during install)
* Validating API connectivity
* Persisting the integration context (`company_id` or other identifiers) **per customer**
* Maintaining the key lifecycle state (reconnect, update, rotate) for each customer
Pleo handles **key generation, pre-scoping, and association with the customer install**, but does **not** manage storage, validation, or reconnect logic. Your integration must implement these steps.
## Installation and Activation Requirements
After the customer provides the Pleo-generated Integrated API Key, the integration must complete an **activation process** to verify connectivity.
### Required Activation Behaviour
Integrations **must**:
* Call a test API endpoint to validate the key
* Confirm that the key is valid and linked to the correct `company_id`
* Store the `company_id` and key securely for future requests
* Prevent the integration from becoming active if validation fails
This ensures the integration is correctly associated with the intended Pleo customer.
### Error Handling Expectations
If activation fails:
* Prevent the integration from entering an active state
* Display actionable error messaging
* Allow the customer to re-enter the key and retry activation
## Company ID Handling
The `company_id` links an Integrated API Key to the correct customer.
### Required Integration Behaviour
Integrations **must**:
* Persist the `company_id` obtained during validation
* Use the correct `company_id` in all subsequent API requests
* Ensure requests are always scoped to the associated customer
### Validation Expectations
Integrations should prevent:
* Using a key with the wrong `company_id`
* Sending requests without proper customer scoping
* Silent connection to a different customer after key replacement
## Permission and Scope Awareness
Integrated API Keys are created with **pre-configured scopes per customer installation**.
Integrations should:
* Validate required scopes during activation
* Surface clear errors if permissions are insufficient
* Avoid assuming full API access
> Pre-scoped permissions reduce risk and simplify integration logic.
## Key Updates and Re-Authentication
Customers may rotate or replace Integrated API Keys via the Pleo Web App.
### Required Integration Behaviour
Integrations **must**:
* Allow entry of a new key to replace the previous credential **for that customer**
* Repeat validation and company association after key replacement
* Confirm required API scopes are present
* Prevent activation if validation fails
### Expected System Behaviour
After a key update:
1. Invalidate previous authentication state for that customer
2. Validate the new key
3. Confirm or update the associated `company_id`
4. Resume normal operation only after successful validation
## Failure and Expiry Handling
Keys may become invalid if they:
* Are rotated or replaced
* Lose required permissions
* Are revoked
Integrations should:
* Detect authentication failures per customer
* Notify users clearly
* Allow immediate replacement and revalidation
## Security Responsibilities
Integrated API Keys are **static, company-level credentials per customer installation**.
Integrations **must**:
* Store keys securely and encrypted at rest
* Mask keys in the UI; avoid logs or analytics exposure
* Rotate or replace compromised keys
* Validate that keys have the required scopes
* Use keys only for the intended customer context
## User Interface / UX Expectations
* Provide a dedicated **secure input field** for the Pleo-generated key
* Mask the key after entry
* Show the connected `company_id` clearly
* Validate the key immediately and confirm connection
* Support **Update / Reconnect flows** for rotated keys
* Provide actionable, non-technical error messaging
## Integrated API Keys vs OAuth 2.0
| UX / Integration Aspect | Integrated API Keys | OAuth 2.0 |
| -------------------------- | ------------------------------------------------- | ---------------------------- |
| Availability | Restricted / approved integrations only | Available |
| Access model | Company-scoped per customer | User-delegated |
| Typical usage | Accounting / ERP single-company per install | Multi-customer integrations |
| Customer consent | Implicit in install | Explicit consent required |
| Multi-customer support | Yes — one integration, multiple customer installs | Yes |
| Authentication | Static pre-scoped key | Access tokens |
| Recommended for production | Approved exceptions | Default for all integrations |
## Typical Development Workflow (Conceptual)
1. Onboard with Pleo and confirm Integrated API Key eligibility
2. Implement secure **connect screen** with key input
3. Validate key with a test API call for each customer
4. Support reconnect / key rotation flows per customer
5. Optional: implement health checks for integration functionality
***
## What Comes Next?
* [User Experience Guidelines for Integrated API Keys](/docs/current/integration-design/auth/integrated-api-keys/integration-design-user-exp-guidelines-for-integrated-api-keys)
***
## Related Reading
* [Integrated API Keys Overview](/docs/current/authentication/integrated-api-keys-overview)
* [Security and Credential Management for Integrated API Keys](/docs/current/integration-design/auth/integrated-api-keys/integration-design-security-for-integrated-api-keys)
* [Integrated API Key Customer Installation Flow](/docs/current/integration-design/auth/integrated-api-keys/integration-design-integrated-api-key-user-install-flow)
* [OAuth 2.0 Overview](/docs/current/authentication/oauth/oauth-overview)
***
# Integrated API Key Customer Installation Flow
Source: https://developers.pleo.io/docs/current/integration-design/auth/integrated-api-keys/integration-design-integrated-api-key-user-install-flow
This article explains **what customers see inside Pleo** when installing an integration that uses **Integrated API Keys**.
It helps integration developers understand:
* Where the API Key originates
* What permissions users approve
* When users are redirected to your product
* When your integration should expect the API Key
## Overview
Integrated API Keys are **generated automatically during installation** of a supported integration.
Customers do not manually create these keys.
Instead, Pleo creates a **customer-specific API Key** after the user grants permissions to the integration.
## High-Level Installation Flow
1. Customer selects an integration in Pleo
2. Pleo displays requested permissions
3. Customer approves access
4. Pleo generates an Integrated API Key
5. Customer copies the API Key
6. Customer is redirected to the integration
7. Customer provides the API Key inside the integration
8. Integration validates the connection
9. Successful activation redirects the user back to Pleo
## Step-by-Step Customer Experience
### 1. Customer initiates installation
In the Pleo Web App:
* Navigate to **Settings**
* Select **Accounting**
* Choose the integration (example: Microsoft Dynamics 365 Finance)
### 2. Permission approval
Pleo displays the permissions required by the integration.
The user must explicitly approve access.
After clicking **Allow**, installation continues.
### 3. Integrated API Key generation
Pleo generates a **customer-specific Integrated API Key**.
The user is shown:
* The API Key
* A copy-to-clipboard button
* Installation instructions
Typical instructions:
1. Copy the API Key above
2. Click the button below to open the integration
3. Log in to the integration
4. Provide the copied API Key
5. Complete connection setup
### 4. Redirect to integration
The user clicks:
**Go to \[Integration Name]**
They are redirected to your product to complete setup.
### 5. Integration activation
Inside your integration:
* User pastes the API Key
* Integration validates connectivity
* Successful validation completes installation
* User may be redirected back to Pleo
## Developer Expectations
Your integration should assume:
* The API Key is copied manually by the customer
* The user may switch tabs during installation
* Validation must occur immediately after entry
* Installation is not complete until validation succeeds
See:
* [Integration Design for Integrated API Keys](/docs/current/integration-design/auth/integrated-api-keys/integration-design-for-integrated-api-keys)
* [User Experience Guidelines for Integrated API Keys](/docs/current/integration-design/auth/integrated-api-keys/integration-design-user-exp-guidelines-for-integrated-api-keys)
## Integrated API Keys vs Standalone API Keys
| Aspect | Integrated API Keys | Standalone API Keys |
| --------------------------- | --------------------------------------------- | ------------------------------------------------- |
| How access begins | Customer installs integration from Pleo | Customer creates API Key in Settings |
| Key creation moment | Generated during installation flow | Generated manually in API Keys settings |
| Permission configuration | Predefined by Pleo integration setup | Selected by customer when creating the key |
| Customer approval | Granted by approving installation permissions | Granted when selecting scopes during key creation |
| Relationship to integration | Key is tied to a specific integration install | Key can be used by any compatible integration |
| Key ownership model | One key per customer installation | One or more keys per company environment |
| Typical usage | Approved accounting / ERP integrations | Custom or developer-built integrations |
***
## Related Reading
* [Integrated API Keys Overview](/docs/current/authentication/integrated-api-keys-overview)
* [Security and Credential Management for Integrated API Keys](/docs/current/integration-design/auth/integrated-api-keys/integration-design-security-for-integrated-api-keys)
* [OAuth 2.0 Overview](/docs/current/authentication/oauth/oauth-overview)
***
# Security and Credential Management for Integrated API Keys
Source: https://developers.pleo.io/docs/current/integration-design/auth/integrated-api-keys/integration-design-security-for-integrated-api-keys
This article defines **security and credential management expectations** when working with **Integrated API Keys**.
It focuses on how integrations must **protect customer-specific credentials**, manage lifecycle events, and maintain secure operation across multiple installations.
Integrated API Keys are **static, pre-scoped company credentials** generated by Pleo during an approved integration install flow.
Each customer installation produces **its own unique key**, and integrations are responsible for securely managing those credentials.
## Security Ownership Model
Security responsibilities are shared between **Pleo** and the **integration developer**.
### Pleo Responsibilities
Pleo manages:
* Key generation during installation
* Permission scoping
* Association between key, integration, and company
* Key visibility within the Pleo Web App
### Integration Responsibilities
Your integration is responsible for:
* Receiving the key securely from the customer
* Validating API connectivity
* Secure storage and protection
* Managing credential lifecycle events
* Handling reconnection and key replacement
Pleo does **not** manage how keys are stored or rotated inside your system.
## Key Security Principles
Integrated API Keys must be treated as **high-sensitivity secrets**.
### Core Principles
* Treat keys like passwords or private tokens
* Store keys only on trusted backend systems
* Apply least-privilege thinking even with pre-scoped permissions
* Prevent exposure through logs, analytics, or client-side code
* Isolate credentials per customer tenant
Because integrations are multi-customer, a compromise must never expose access to other tenants.
## Secure Storage Requirements
Each Integrated API Key represents access to **one customer company** and must be stored securely.
### Required Practices
Integrations must:
* Store keys encrypted at rest
* Persist keys only in backend systems
* Mask keys after initial entry
* Prevent retrieval of full keys after saving
* Segregate credentials per customer tenant
Keys must **never** be embedded in:
* Frontend applications
* Mobile apps
* Browser storage
* Client-side JavaScript
### Recommended Storage Options
* Managed secrets vaults (AWS Secrets Manager, HashiCorp Vault)
* Encrypted environment variables
* Secure server-side credential stores
## Transmission and Handling
When transmitting or processing Integrated API Keys:
* Always use **HTTPS**
* Send keys using headers, not query parameters
* Avoid temporary storage in logs or debug output
* Validate format before submission where possible
Keys should only ever pass through secure backend channels.
## Multi-Customer Credential Isolation
Integrated API Keys are **per installation**, meaning your integration will store multiple keys.
Your system must ensure:
* Each key maps to exactly one customer tenant
* Keys are never shared across customers
* Requests always use the correct associated `company_id`
* Credential access is scoped internally by tenant context
Failure to isolate credentials may result in cross-company data exposure.
## Permission and Scope Awareness
Integrated API Keys are created with **pre-configured scopes** defined by Pleo.
Integrations should:
* Verify required permissions during activation
* Surface clear errors if permissions are insufficient
* Avoid assuming unrestricted API access
Pre-scoped permissions simplify implementation but do not remove security responsibility.
## Key Rotation and Replacement
Customers may rotate or recreate Integrated API Keys within the Pleo Web App.
Integrations must support secure replacement.
### Required Workflow
1. Customer generates a replacement key in Pleo
2. User updates the key inside the integration
3. Integration revalidates API connectivity
4. Associated `company_id` is confirmed
5. Previous credential is invalidated internally
Automated monitoring for authentication failures is strongly recommended.
## Compromise and Incident Response
If a key is suspected to be exposed:
Integrations must:
* Immediately disable affected authentication
* Prompt the user to replace the key
* Revalidate the new credential
* Review logs for suspicious activity
Because keys are tenant-scoped, incident handling should remain isolated to the affected customer.
## Logging and Monitoring
Logging must balance observability with credential protection.
### Required Behaviour
* Never log full API Keys
* Mask sensitive values
* Store only minimal identifiers if required (e.g. last 4 characters)
* Monitor authentication failures and permission errors
### Recommended Monitoring
* Detect repeated authentication failures
* Alert users when reconnection is required
* Surface degraded integration states clearly
## Lifecycle Expectations
Integrated API Keys may become invalid if they:
* Are rotated or replaced
* Are revoked
* Lose required permissions
* Become disconnected during configuration changes
Integrations should detect failures quickly and guide users toward recovery.
## Summary
Integrated API Keys introduce a shared security model:
* Pleo manages **generation and permission scoping**
* Integrations manage **storage, lifecycle, and operational security**
* Each customer installation requires **independent credential handling**
Following secure storage, tenant isolation, and lifecycle management practices ensures integrations remain safe, predictable, and scalable across multiple customers.
***
## What Comes Next?
* [Integrated API Key Customer Installation Flow](/docs/current/integration-design/auth/integrated-api-keys/integration-design-integrated-api-key-user-install-flow)
***
## Related Reading
* [Integrated API Keys Overview](/docs/current/authentication/integrated-api-keys-overview)
* [User Experience Guidelines for Integrated API Keys](/docs/current/integration-design/auth/integrated-api-keys/integration-design-user-exp-guidelines-for-integrated-api-keys)
* [OAuth 2.0 Overview](/docs/current/authentication/oauth/oauth-overview)
***
# User Experience Guidelines for Integrated API Keys
Source: https://developers.pleo.io/docs/current/integration-design/auth/integrated-api-keys/integration-design-user-exp-guidelines-for-integrated-api-keys
This article defines **user experience (UX) expectations** for integrations that use **Integrated API Keys**.
It focuses on **how users interact with Integrated API Key authentication** inside your product.
## UX Principles
Integrated API Keys behave differently from OAuth 2.0 authentication.
Users are providing a **customer-specific installation credential**, not logging into a session.
Your interface should therefore prioritise:
* Clear guidance on where the key comes from
* Safe credential handling
* Explicit confirmation of company connection
* Predictable recovery when authentication fails
## API Key Input Experience
Users must have a clear and secure way to provide their Integrated API Key.
### Required Behaviour
Your UI must:
* Provide a dedicated API Key input field
* Clearly label the field as a **secret credential**
* Accept secure paste input
* Mask the API Key during entry where possible
* Never display the full key after saving
### Recommended Guidance Text
Explain to users that:
* The API Key was **generated by Pleo during the installation process**
* The key grants **company-level access to the integration**
* The key should be stored securely
* The key cannot be viewed again after saving
## Saving and Confirmation Behaviour
After a user submits an API Key, the system should immediately validate it.
### Expected UX Flow
1. User copies the Integrated API Key from Pleo
2. User enters the API Key into the integration
3. User clicks **Connect** or **Save**
4. System validates authentication
5. Company association is confirmed
6. Integration becomes active
Users should never be left unsure whether authentication succeeded.
## Company Confirmation
Integrated API Keys are **linked to a specific Pleo company installation**.
Users should be able to confirm the organisation associated with the connection.
### Required Behaviour
Your UI must:
* Display the associated `company_id` after activation
* Clearly indicate which company is connected
* Confirm successful installation visually
### Example Confirmation Signals
* “Connected to Company ID: XXXXX”
* Success status indicator
* Connected state badge
This prevents accidental connections to the wrong organisation.
## Error Handling Experience
Authentication failures must be clearly communicated.
### Errors Should:
* Explain what failed
* Avoid technical jargon where possible
* Provide a clear recovery action
### Examples
Good:
* “The API Key appears to be invalid. Please check and try again.”
* “This key does not have the required permissions.”
Avoid:
* Generic messages such as “Authentication failed.”
### Required Behaviour
If activation fails:
* The integration must not enter an active state
* Users must be able to retry immediately
* The API Key field must remain editable
## API Key Visibility Rules
Integrated API Keys must be treated as sensitive secrets within the interface.
### Required UX Behaviour
* Mask the API Key after saving
* Never allow full key retrieval
* Do not expose keys in logs or UI messages
* Avoid displaying partial keys unless necessary for identification
If identification is needed, show only minimal characters (for example: `••••abcd`).
## Updating API Keys
Customers may rotate or replace Integrated API Keys.
Users must be able to replace an API Key when required.
### Required UX Behaviour
Your interface must:
* Provide an **Update API Key** or **Reconnect** action
* Allow entry of a replacement key
* Re-run validation automatically
* Clearly show reconnection progress
### Expected User Flow
1. User selects **Update API Key**
2. User enters new key
3. System revalidates authentication
4. Company association is confirmed again
5. Integration returns to active state
## Expiry and Failure Recovery
Integrated API Keys may stop working if they are **revoked, rotated, or lose permissions**.
Your UI should help users recover quickly.
### Recommended Behaviour
* Detect authentication failures automatically
* Display persistent connection warnings
* Provide a clear **Replace API Key** or **Reconnect** action
* Avoid silent failures
Example messaging:
> “Your connection to Pleo has been interrupted. Please update your API Key to restore access.”
## Status and Connection Visibility
Users should always understand the current integration state.
### Recommended Connection States
* Connected
* Validation in progress
* Connection failed
* Action required
Avoid ambiguous states where users cannot determine whether the integration is functioning.
## Relationship to OAuth 2.0 UX
OAuth 2.0 and API Keys create different mental models for users.
| UX Aspect | OAuth 2.0 | Integrated API Keys |
| ---------------- | --------------- | ----------------------- |
| User action | Login & consent | Paste credential |
| Ownership | User account | Installation credential |
| Renewal | Automatic | Manual replacement |
| Failure recovery | Reconnect login | Replace key |
| Session concept | Yes | No |
Design interfaces accordingly.
***
## What Comes Next?
* [Security and Credential Management for Integrated API Keys](/docs/current/integration-design/auth/integrated-api-keys/integration-design-security-for-integrated-api-keys)
***
## Related Reading
* [Integrated API Keys Overview](/docs/current/authentication/integrated-api-keys-overview)
* [Integrated API Key Customer Installation Flow](/docs/current/integration-design/auth/integrated-api-keys/integration-design-integrated-api-key-user-install-flow)
* [OAuth 2.0 Overview](/docs/current/authentication/oauth/oauth-overview)
***
# Connection and Authorisation Overview
Source: https://developers.pleo.io/docs/current/integration-design/auth/integration-design-auth-overview
This section explains how integrations establish and maintain a **secure connection** between Pleo and an external system.
Connection and authorisation ensure that:
* requests are authenticated securely
* permissions are correctly applied
* data is exchanged with the correct company
* integrations operate reliably over time
Pleo supports multiple authentication models designed for different integration scenarios.
## Authentication Policy Overview
All authentication methods are technically supported in both **Staging** and **Production** environments. The primary difference between methods is **who may use them and under what conditions**.
| If your integration… | Use |
| ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Connects multiple customers and supports user consent flows | [**OAuth 2.0 (Required for Partners)**](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview) |
| Is an approved accounting or ERP integration that cannot support OAuth 2.0 redirects | [**Integrated API Keys (Approved Exception)**](/docs/current/integration-design/auth/integrated-api-keys/integration-design-for-integrated-api-keys) |
| Requires controlled API access for testing or internal tooling | [**Standalone API Keys (Limited Availability)**](/docs/current/integration-design/auth/api-keys/integration-design-for-standalone-api-keys) |
## High-level Comparison
Below is a high-level overview of the characteristics of each authentication method.
| Aspect | OAuth 2.0 | Integrated API Keys | Standalone API Keys |
| --------------------------------------- | ----------------------------- | ------------------------------------------------ | -------------------------------- |
| Recommended for production integrations | ✅ Yes | ⚠️ Approved exceptions | ⚠️ Limited |
| Multi-customer integrations | ✅ Yes | ✅ Yes (one key per installation) | ⚠️ Limited |
| How access is granted | Redirect and user consent | Installation approval in Pleo | Manual key creation |
| Credential type | Access token + refresh token | Installation-scoped API key | Manually created API key |
| Credential lifecycle | Automatic token refresh | Manual rotation if replaced | Manual rotation |
| Typical usage | Most marketplace integrations | Accounting/ERP systems without OAuth 2.0 support | Internal tooling and API testing |
## Credential Handling Principles
While implementation details vary by authentication method, the following principles apply to all integrations:
* Credentials, including **access tokens, refresh tokens, and API keys**, must be treated as sensitive secrets.
* Integrations must implement responsible credential management.
* Integrations should maintain long-lived operation without repeated manual intervention.
* Authentication failures must be detected and handled gracefully.
Each integration represents a **one-to-one connection** between:
* a Pleo company, and
* a corresponding company or tenant in the external system.
Correct entity association ensures data is synchronised with the appropriate accounts, as defined by the **[Multi-Entity model](/docs/current/platform/multi-entity/multi-entity-and-api-scope)**.
***
## Related Reading
**OAuth 2.0**
* **[OAuth 2.0 Overview](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview)** – recommended authentication model
* **[OAuth 2.0 Token Lifecycle](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview)** – managing OAuth tokens securely
* **[OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)** – step-by-step OAuth implementation guide
**API Keys**
* **[Integrated API Keys Overview](/docs/current/authentication/integrated-api-keys-overview)** – authentication for approved integrations
* **[Standalone API Keys Overview](/docs/current/authentication/standalone-api-keys-overview)** – API keys for staging exploration
***
# Multi-Entity Overview
Source: https://developers.pleo.io/docs/current/integration-design/auth/multi-entity/integration-design-auth-multi-entity-overview
Integrations must accurately link a Pleo entity to the corresponding company
in the accounting system, ensuring that all data exchange occurs correctly
and that transactions are recorded in the proper accounts.
## Key Principles
* The integration is always a **1-to-1 connection**: one Pleo entity maps to one
company in the Accounting System.
* The user should be able to **select the appropriate company** from their
accounting system when connecting their Pleo account.
* Correct entity mapping is critical for accurate bookkeeping and reporting.
For details on how Pleo represents companies and organisations (and how API access is scoped) see [Multi-Entity and API Scope](/docs/current/platform/multi-entity/multi-entity-and-api-scope).
# OAuth 2.0 Client Configuration
Source: https://developers.pleo.io/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration
Once your OAuth 2.0 client is registered with Pleo and you have received your **Client ID** and **Client Secret**, you must configure your **OAuth 2.0 client** to communicate with Pleo’s Authorisation Server.
OAuth 2.0 client [registration establishes](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-registration) your **OAuth 2.0 client’s identity** with Pleo.
OAuth 2.0 client configuration enables your integration to perform the OAuth 2.0 protocol, including:
* Redirecting users to Pleo for authorisation
* Receiving authorisation codes
* Exchanging codes for access and refresh tokens
* Refreshing tokens securely
* Calling Pleo APIs on behalf of authorised users
This configuration is completed within your **OAuth 2.0 client** using your chosen OAuth 2.0 library or framework.
## When You Need This
You should complete OAuth 2.0 client configuration after:
* Registering your OAuth 2.0 client with Pleo
* Receiving your **Client ID** and **Client Secret**
* Selecting an OAuth 2.0 client library or framework
This page describes **what must be configured**, not how to implement it in a specific programming language.
## Configuration Checklist
Ensure your **OAuth 2.0 client** is configured with:
* Client ID
* Authorisation endpoint
* Token endpoint
* Redirect URI (must match registered values exactly)
* Requested scopes
* PKCE enabled (S256)
## Required OAuth 2.0 Configuration Parameters
Configure your OAuth 2.0 client using the following values:
| Parameter | Value |
| ---------------------- | -------------------------------------------- |
| Authorisation Endpoint | `{AUTHORIZATION_SERVER_URL}/oauth/authorize` |
| Token Endpoint | `{AUTHORIZATION_SERVER_URL}/oauth/token` |
| Grant Type | Authorisation Code (with PKCE) |
| PKCE | Required (S256 only) |
| Redirect URI | One of the registered redirect URIs |
| Scopes | Approved scopes from client registration |
Exact configuration keys depend on the OAuth 2.0 library you use. Refer to your library’s documentation for precise mappings.
## PKCE Requirements
Pleo requires [**Proof Key for Code Exchange (PKCE)**](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-pkce-and-secured-patterns) where supported by the client.
* Only the **S256** code challenge method is supported
* Plain (`plain`) PKCE is not allowed
* PKCE is **mandatory for public clients** (SPAs, mobile apps)
* PKCE is **strongly recommended for confidential clients**
Ensure your OAuth 2.0 library is configured to:
* Generate a `code_verifier`
* Send the corresponding `code_challenge` during authorisation
* Provide the `code_verifier` during token exchange
## Authorisation Server URLs
Replace `{AUTHORIZATION_SERVER_URL}` with the base URL of the environment you are targeting:
| Environment | Authorisation Server URL |
| ----------- | ------------------------------ |
| Staging | `https://auth.staging.pleo.io` |
| Production | `https://auth.pleo.io` |
Always test your integration fully in **staging** before switching to production.
## Redirect URI Configuration
Your OAuth 2.0 client must use redirect URIs that:
* Exactly match the URIs registered with Pleo
* Are publicly accessible (except `localhost` for development)
* Use HTTPS in production environments
* Do not contain wildcards
If your integration supports multiple post-login destinations, use the OAuth 2.0 `state` parameter to route users internally **after** authorisation.
## Client Authentication Method
Pleo supports **client authentication at the token endpoint only for confidential clients**.
* Supported method: `client_secret_basic`
Confidential clients must:
* Send the Client ID and Client Secret via HTTP Basic Authentication
* Store the Client Secret securely on the server
* Never expose the Client Secret in frontend or public code
Public clients (SPAs, mobile apps):
* **Must not use a client secret**
* Must rely on PKCE for security
## What This Configuration Enables
Once configured correctly, your OAuth 2.0 client can:
* Redirect users to Pleo for authorisation
* Receive authorisation codes securely
* Exchange codes for access and refresh tokens
* Refresh access tokens without re-prompting users
* Access Pleo APIs on behalf of authorised users
## FAQs
These terms describe different parts of how OAuth 2.0 works in the Pleo platform.
The difference between an **integration**, **application**, and **OAuth 2.0 client** is explained in the [OAuth 2.0 Concepts and Terminology](/docs/current/authentication/oauth/oauth-overview#concepts-and-terminology) section.
## Related Reading
* [OAuth 2.0 Libraries and Standards](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-libraries-and-standards)
* [Token Lifecycle](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview)
* [OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)
# OAuth 2.0 Client Registration
Source: https://developers.pleo.io/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-registration
Integrations using **OAuth 2.0** must register a client with Pleo. Client registration establishes your **OAuth 2.0 client’s identity** and provides the credentials required to securely authenticate users and request access tokens.
Once registered, your **application** can redirect users to Pleo to grant consent and obtain tokens for accessing Pleo APIs.
## Why OAuth 2.0 client registration is required
Client registration enables Pleo to:
* Identify your **OAuth 2.0 client** during authorisation flows
* Display your **application’s** name and branding to users
* Enforce security controls such as redirect URI validation
* Restrict access using requested API [scopes](/docs/current/authentication/api-scopes)
* Issue tokens securely to authorised **OAuth 2.0 clients**
Without client registration, your integration cannot participate in the OAuth 2.0 authorisation flow.
## Information required during registration
Pleo requires three categories of information.
### 1. Human-readable information
This information is displayed to users when they authorise your application.
Do **not** use Pleo branding in your client information.
* **Client Name**: Your application's name as shown to users.
* **Client URI**: Homepage or landing page of your application.
* **Logo**: Square (1:1), high-resolution logo representing your application.
* **Terms of Service URI**: Legal agreement governing use of your application.
* **Privacy Policy URI**: Explains how user data is handled.
* **Contact Email(s)**: Contact details for responsible developers or support.
### 2. Pleo-specific integration information
These URIs allow Pleo and users to interact with your application.
* **Initiation URI**: Page where users begin connecting your application with Pleo.
* **Settings URI**: Page in your application where users manage or update configuration settings for the integration (for example, accounting periods, mappings, or synchronisation preferences).
### 3. Technical information
This information is required for the OAuth 2.0 protocol.
* **Redirect URI(s)**\
OAuth 2.0 callback endpoints where Pleo redirects users after authorisation.
Requirements:
* Must use HTTPS in production
* HTTP allowed only for localhost during development
* Must match exactly (aliases or wildcards are not supported)
* **Requested scopes**\
Permissions your **OAuth 2.0 client** requests to access Pleo APIs. See [API Scopes](/docs/current/authentication/api-scopes).
* **PKCE support**\
Required for public clients (SPAs, mobile apps).
Requirements:
* Must use `S256`
* `plain` is not supported
* **Subject type preference (optional)**\
Defines the default resource context used by your **OAuth 2.0 client**.
## Credentials issued after registration
After successful registration, Pleo provides:
### Client ID
Public identifier of your **OAuth 2.0 client**.
Used to:
* Identify your OAuth 2.0 client during authorisation
* Associate tokens with your client
This value is safe to expose in client-side applications.
### Client Secret
Confidential credential used to authenticate your **OAuth 2.0 client** when requesting tokens.
Security requirements:
* Must be stored securely on a server
* Must never be exposed in browser or mobile code
* Must never be committed to source control
Public clients using PKCE may not require a client secret.
## Redirect URI security requirements
Redirect URIs are strictly validated to prevent token interception.
Requirements:
* Must be registered exactly
* Must be publicly accessible (except localhost for development)
* Must use the `state` parameter to prevent CSRF attacks
* Must not use wildcards
Example:
```html theme={null}
https://example.com/oauth/callback
```
## Testing your OAuth 2.0 client
Before production use:
* Verify redirect URIs are registered correctly
* Complete the full authorisation flow
* Confirm access tokens can be retrieved successfully
* Test token refresh behaviour
Tools such as Postman can be used for testing.
## Outcome
After registration:
* Your application is recognised by Pleo
* Your application can request user authorisation
* Your OAuth 2.0 client can obtain access tokens securely
* Your integration is ready for OAuth 2.0 implementation
## FAQs
These terms describe different parts of how OAuth 2.0 works in the Pleo platform.
The difference between an **integration**, **application**, and **OAuth 2.0 client** is explained in the [OAuth 2.0 Concepts and Terminology](/docs/current/authentication/oauth/oauth-overview#concepts-and-terminology) section.
## Related Reading
* [Client Configuration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration)
* [OAuth 2.0 Libraries and Standards](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-libraries-and-standards)
* [PKCE and Secured Patterns](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-pkce-and-secured-patterns)
* [OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)
# OAuth 2.0 Libraries and Standards
Source: https://developers.pleo.io/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-libraries-and-standards
Pleo integrations are strongly encouraged to use **industry-standard OAuth 2.0 client libraries** rather than implementing OAuth 2.0 flows from scratch.
OAuth 2.0 is a security-critical protocol. Using a mature, well-maintained library significantly reduces the risk of implementation errors, security vulnerabilities, and non-standard behaviour.
## Why Use an OAuth 2.0 Library
Implementing OAuth 2.0 manually is complex and error-prone. Established libraries help ensure that your integration:
* Adheres to OAuth 2.0 standards
* Correctly handles redirects, tokens, and error conditions
* Applies security best practices by default (for example, PKCE and CSRF protection)
* Remains maintainable as standards evolve
Using a library allows you to focus on your product logic rather than protocol details.
## What OAuth 2.0 Libraries Typically Handle
Most OAuth 2.0 client libraries provide built-in support for:
* Generating authorisation requests (including `state` and PKCE parameters)
* Redirecting users to the authorisation server
* Handling authorisation responses and errors
* Exchanging authorisation codes for access and refresh tokens
* Refreshing access tokens
* Tracking token expiry
* Protecting against CSRF and replay attacks
Many libraries also allow customisation, such as:
* Integrating with your web framework’s routing and redirect mechanisms
* Plugging in your own persistence layer for tokens and transient values (for example `state` and `code_verifier`)
* Customising success and error handling
* Hooking into lifecycle events during the OAuth 2.0 flow
Exact features and extension points depend on the library you choose. Always consult the documentation of your selected library.
## Recommended OAuth 2.0 Client Libraries
The following libraries are commonly used, well-supported, and suitable for production OAuth 2.0 integrations:
### Java
* `spring-security-oauth2-client`
### Python
* `Authlib`
* `Requests-OAuthlib`
### JavaScript
* OAuth 2.0 strategies for `passport.js`
* Standards-compliant OAuth 2.0 / OIDC client libraries appropriate to your framework
### .NET
* `Microsoft.Identity.Web`
These libraries are widely adopted and actively maintained. Equivalent standards-compliant libraries may be used in other ecosystems.
## OAuth 2.0 Standards and Compliance
Pleo’s OAuth 2.0 implementation follows **standard OAuth 2.0 specifications and best practices**, including:
* **RFC 6749 – OAuth 2.0 Authorisation Framework**
* Proof Key for Code Exchange (PKCE)
* Standard token and refresh flows
No proprietary extensions or custom protocol behaviour are required to integrate with Pleo.
Integrations should avoid custom or non-standard OAuth 2.0 implementations, as these can introduce security risks and compatibility issues.
## What You Still Need to Implement
Even when using an OAuth 2.0 library, integrations are responsible for:
* Providing a user interface to initiate the connection (for example, a “Connect to Pleo” button)
* Persisting access and refresh tokens securely
* Handling token lifecycle events (expiry, refresh failures, revocation)
* Implementing appropriate error handling and user messaging
* Designing a token refresh strategy that fits your system architecture
These responsibilities are covered in more detail in the authentication workflow and token lifecycle guides.
## FAQs
These terms describe different parts of how OAuth 2.0 works in the Pleo platform.
The difference between an **integration**, **application**, and **OAuth 2.0 client** is explained in the [OAuth 2.0 Concepts and Terminology](/docs/current/authentication/oauth/oauth-overview#concepts-and-terminology) section.
## Related Reading
* [Client Configuration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration)
* [OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)
# PKCE and Secured Patterns
Source: https://developers.pleo.io/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-pkce-and-secured-patterns
OAuth 2.0 integrations with Pleo must follow established security patterns to protect users, credentials, and tokens throughout the authorisation lifecycle.
This page explains **why** these patterns exist and **when** they apply. Detailed configuration and implementation guidance is covered in the linked pages.
## Proof Key for Code Exchange (PKCE)
**PKCE (Proof Key for Code Exchange)** protects authorisation code flows from interception and replay attacks.
It is particularly important for **public clients**, where a client secret cannot be kept confidential.
### When PKCE Is Required
* **Mandatory** for public clients:
* Single Page Applications (SPAs)
* Mobile applications
* Strongly recommended for all OAuth 2.0 clients where supported
Only the **S256** code challenge method is supported. The `plain` method is not allowed.
PKCE is configured as part of your OAuth 2.0 client setup and handled automatically by most standards-compliant OAuth 2.0 libraries. See *Client Configuration* and *OAuth 2.0 Libraries and Standards* for details.
## Secure Transport (HTTPS)
All OAuth 2.0-related communication **must occur over HTTPS**, including:
* Authorisation requests
* Token exchanges
* Token refresh requests
* API calls using access tokens
Using HTTPS prevents interception of authorisation codes and tokens in transit.
HTTP is permitted only for `localhost` redirect URIs in development environments.
## Client Classification and Responsibilities
OAuth 2.0 security requirements vary depending on client type:
* **Public clients**\
Cannot safely store a client secret. Must rely on PKCE and secure redirect handling.
* **Confidential clients**\
Can securely store a client secret and must authenticate at the token endpoint using `client_secret_basic`.
Understanding your client type is essential before implementing OAuth 2.0 flows. This distinction is covered in the *OAuth 2.0 Authentication Workflow*.
## Avoiding Common Security Pitfalls
Integrations should **avoid** the following anti-patterns:
* Implementing OAuth 2.0 flows manually instead of using a standard library
* Storing tokens in insecure locations (for example, browser local storage)
* Reusing expired or replaced refresh tokens
* Skipping PKCE for public clients
* Hardcoding secrets in frontend or distributed code
Correct token handling and storage are covered in the Token Lifecycle section.
## Related Reading
* [Token Lifecycle](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview)
* [Client Configuration](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration)
* [OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)
# OAuth 2.0 Overview
Source: https://developers.pleo.io/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview
Integrations with Pleo should use **OAuth 2.0** for authorisation wherever possible. Alternative methods may be supported, such as [Integrated API Keys](/docs/current/getting-started/developer-partnership-programme#authentication-requirements).
## OAuth 2.0 Workflow
OAuth 2.0 uses a redirect-based authorisation flow that allows users to grant your integration access to their Pleo data without sharing credentials.
At a high level, OAuth 2.0 works as follows:
1. **Resource Owner + Client**\
A customer clicks the "Connect with Pleo" button in your application.
2. **Authorisation Server**\
The request is redirected to Pleo’s Authorisation Server, where the user grants permission for your integration to access their Pleo data.
3. **Client**\
Once the user authorises, Pleo redirects them back to your application with a code. Your integration exchanges this code for an access token.
4. **Resource Server + Client**\
After the exchange, your integration can call Pleo APIs by including the access token in API requests. Your integration can then display the data to users or process it for downstream workflows, such as exports, synchronisation, or reporting.
## What you need at the implementation level
This section outlines the key areas you need to understand to implement OAuth 2.0 securely:
### 1. Getting Set Up
* [**Client Registration**](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-registration): Register your OAuth 2.0 client with Pleo. Provide required details such as redirect URIs and requested API [scopes](/docs/current/authentication/api-scopes).
* [**Client Configuration**](/docs/current/integration-design/auth/oauth/getting-set-up/oauth-client-configuration): Ensure your OAuth 2.0 client is configured to communicate correctly with Pleo’s Authorisation Server.
### 2. Implementing OAuth
* [**Libraries and Standards**](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-libraries-and-standards): Use a standards-compliant OAuth 2.0 library to avoid common pitfalls.
* [**PKCE and Secured Patterns**](/docs/current/integration-design/auth/oauth/implementing-oauth/integration-design-auth-oauth-pkce-and-secured-patterns): Follow established security patterns to protect user credentials and tokens during the authorization flow.
### 3. Token Lifecycle
* Understand how [**access tokens** and **refresh tokens**](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview) are issued, stored, refreshed, and revoked securely.
## What Comes Next?
* OAuth 2.0 Setup Workflow Guide
When ready, follow the [**step-by-step implementation**](/docs/current/guides/oauth-workflow-guide) guide
## FAQs
These terms describe different parts of how OAuth 2.0 works in the Pleo platform.
The difference between an **integration**, **application**, and **OAuth 2.0 client** is explained in the [OAuth 2.0 Concepts and Terminology](/docs/current/authentication/oauth/oauth-overview#concepts-and-terminology) section.
## Related Reading
* **[OAuth 2.0 Access to Staging Workflow Guide](/docs/current/guides/environment-access/staging-oauth-workflow)**\
Understand how Pleo Partners gain access to Pleo’s Staging environment and prepare their integration to authenticate using OAuth 2.0.
# Centralised Token Refresh
Source: https://developers.pleo.io/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-centralised-token-refresh
Integrations must implement a **centralised mechanism** to refresh access tokens using the refresh token.
Refreshing tokens from multiple locations or concurrent requests can result in **token invalidation**, race conditions, and failed API calls.
## Why Centralised Token Refresh Is Required
When a refresh token is used:
* Pleo issues a **new access token and a new refresh token**
* **All previous tokens are immediately invalidated**
If multiple parts of your system attempt to refresh tokens independently, one refresh may invalidate tokens still in use elsewhere.
A single refresh authority ensures token consistency across your integration.
## Core Requirements
Your token refresh mechanism must:
* Monitor the `expires_in` field returned with access tokens
* Refresh tokens **before** access tokens expire
* Ensure **only one refresh request** is in flight at a time
* Update and persist **both** the access token and refresh token on success
* Immediately discard all previously issued tokens
## Retry and Failure Handling
* Retry refresh requests using **exponential backoff** for transient failures\
(for example: network timeouts or temporary service unavailability)
* Do **not** retry indefinitely
* If refresh fails due to an invalid or expired refresh token:
* Stop retrying
* Restart the OAuth 2.0 flow
* Prompt the user to reauthenticate
## Concurrency and Race Condition Prevention
To prevent multiple refresh attempts:
* Use locking, queuing, or a shared refresh promise/future
* Ensure all API requests depend on the same refreshed token result
* Block or delay outgoing API calls while a refresh is in progress
## Outcome
* Tokens remain consistent across your system
* Refresh token invalidation errors are avoided
* Users are only prompted to reauthenticate when strictly necessary
## Related Reading
* **[OAuth 2.0 Overview](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview)** - introduction to OAuth 2.0 for Pleo integrations
* **[API Keys Overview](/docs/current/authentication/standalone-api-keys-overview)** – alternative authentication method
* **[Tokens Overview](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview)** – understanding access and refresh tokens
* **[Secure Token Storage](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-secure-token-storage)** – storing credentials securely
* **[Race Condition Prevention](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-race-condition-prevention)** – avoid duplicate token refresh attempts
* **[OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)** - Step-by-step guide to configure OAuth 2.0 for your integration
# Race Condition Prevention
Source: https://developers.pleo.io/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-race-condition-prevention
When multiple processes or threads detect that an OAuth 2.0 access token is expired, attempting to refresh the token simultaneously can cause **race conditions**. This may result in:
* Invalid or revoked refresh tokens
* Failed API calls due to stale tokens
* Inconsistent token state across different processes or servers
Proper handling of race conditions ensures your integration refreshes tokens safely and reliably.
## Recommended Patterns
### Single Refresh Lock
Allow only **one process** to perform a token refresh at a time. Other processes must wait until the new access and refresh tokens are available.
### Atomic Token Update
After a successful refresh, update access and refresh tokens **atomically** in your storage system. This prevents other processes from using partially updated tokens.
### Exponential Backoff for Waiting Processes
Processes waiting on the refresh lock should retry safely using an exponential backoff strategy to avoid overwhelming the token endpoint.
### Centralised Refresh Service
Consider implementing a **dedicated service** or singleton responsible for refreshing tokens. Other parts of your system should query this service for the latest token instead of refreshing independently.
## Implementation Tips
* **Server-side applications:** Use a distributed lock (e.g., Redis lock) when running multiple instances.
* **Multi-threaded applications:** Use a mutex, semaphore, or similar concurrency control.
* **Logging and monitoring:** Track refresh attempts, lock waits, and failures to detect potential race conditions.
## What to Avoid
* Performing simultaneous token refreshes across threads or processes
* Allowing old or invalid tokens to persist after a refresh
* Ignoring errors during refresh, which can lead to inconsistent token state
## Related Reading
* **[OAuth 2.0 Overview](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview)** - introduction to OAuth 2.0 for Pleo integrations
* **[API Keys Overview](/docs/current/authentication/standalone-api-keys-overview)** – alternative authentication method
* **[Tokens Overview](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview)** – understanding access and refresh tokens
* **[Secure Token Storage](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-secure-token-storage)** – storing credentials securely
* **[Centralised Token Refresh](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-centralised-token-refresh)** – safe token refresh patterns
* **[OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)** - Step-by-step guide to configure OAuth 2.0 for your integration
# Handling Refresh Token Expiry or Revocation
Source: https://developers.pleo.io/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-refresh-token-expiry-revocation
Refresh tokens are long-lived credentials that allow your integration to obtain new access tokens without requiring users to reauthenticate. However, **refresh tokens can expire or be revoked** due to security policies, user actions, or other events. Integrations must handle these situations gracefully.
## Detecting Expired or Revoked Refresh Tokens
* Failed token refresh attempts typically indicate expiry or revocation.
* The token endpoint will return an error response such as:
* `invalid_grant`
* `invalid_token`
* Do **not** attempt to reuse the expired or revoked token.
## Recovery Strategies
When a refresh token is no longer valid:
1. **Redirect Users to Reauthenticate**
* Start a new OAuth 2.0 authorisation flow.
* Prompt the user to grant consent again, generating a new access and refresh token pair.
* Preserve the user’s context if possible (e.g., use the `state` parameter to remember post-login destination).
2. **Fallback Mechanisms**
* Implement temporary measures to avoid broken sessions. For example:
* Queue API requests until a new token is obtained.
* Display a friendly message prompting the user to reconnect.
3. **Secure Cleanup**
* Immediately discard the expired or revoked refresh token.
* Ensure no copies remain in storage or memory to prevent accidental reuse.
## Best Practices
* [Centralise token refresh logic](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-centralised-token-refresh) to reduce errors and race conditions.
* Monitor refresh failures to detect potential security incidents.
* Ensure users can recover with minimal friction to maintain trust and continuity.
* Use consistent logging and error handling to aid debugging and operational monitoring.
## Related Reading
* **[OAuth 2.0 Overview](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview)** - introduction to OAuth 2.0 for Pleo integrations
* **[API Keys Overview](/docs/current/authentication/standalone-api-keys-overview)** – alternative authentication method
* **[Tokens Overview](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview)** – understanding access and refresh tokens
* **[Secure Token Storage](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-secure-token-storage)** – storing credentials securely
* **[Race Condition Prevention](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-race-condition-prevention)** – avoid duplicate token refresh attempts
* **[OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)** - Step-by-step guide to configure OAuth 2.0 for your integration
# Secure Token Storage
Source: https://developers.pleo.io/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-secure-token-storage
OAuth 2.0 access and refresh tokens are sensitive credentials that grant access to user data. **Storing tokens securely is critical to maintaining the integrity of your integration implementation and protecting user data.**
Improper storage can lead to:
* Account compromise if tokens are stolen
* Unauthorized API access
* Broken integrations due to revoked or leaked tokens
## Recommended Storage Practices
### Frontend Clients (SPAs, Mobile Apps)
* Use **HTTP-only, Secure cookies** for storing tokens in web applications.
* Use platform-specific **secure key stores** (e.g., iOS Keychain, Android Keystore) for mobile apps.
* Never store tokens in **local storage, session storage, or plain text files**.
### Backend Applications / Server-Side
* Store tokens in **encrypted databases** or **secure vaults**.
* Apply **strict access controls** to token storage.
* Rotate encryption keys and credentials periodically.
### General Security Guidelines
* **Encrypt tokens at rest** and in transit.
* **Avoid logging tokens** or including them in error messages.
* Implement **token revocation and cleanup** procedures for inactive users or expired tokens.
* Use a [**centralised token refresh mechanism**](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-centralised-token-refresh) to minimise the number of storage locations.
## Monitoring and Auditing
* Track access and refresh token usage to detect suspicious activity.
* Monitor for failed refresh attempts, which may indicate expired or compromised tokens.
## Related Reading
* **[OAuth 2.0 Overview](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview)** - introduction to OAuth 2.0 for Pleo integrations
* **[API Keys Overview](/docs/current/authentication/standalone-api-keys-overview)** – alternative authentication method
* **[Tokens Overview](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview)** – understanding access and refresh tokens
* **[Centralised Token Refresh](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-centralised-token-refresh)** – safe token refresh patterns
* **[Race Condition Prevention](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-race-condition-prevention)** – avoid duplicate token refresh attempts
* **[OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)** - Step-by-step guide to configure OAuth 2.0 for your integration
# Access Token Introspection
Source: https://developers.pleo.io/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-introspection
Access token introspection allows your integration to **verify whether an OAuth 2.0 token is currently valid** and retrieve server-authoritative metadata about it.
Introspection is intended for **debugging, validation, and internal decision-making**. Integrations must **not rely on token contents locally**, as tokens remain opaque outside of this endpoint. See the [Tokens Overview](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview) for guidance on token handling expectations.
## When to Use Introspection
Use token introspection when you need to:
* Confirm whether a token is still active
* Debug authentication or authorisation failures
* Verify scopes or audience during development or support workflows
Do **not** use introspection as a replacement for normal token lifecycle handling (expiry tracking and refresh).
## Token Introspection Endpoint
| Environment | Endpoint URI |
| ----------- | ----------------------------------------------------- |
| Staging | `https://auth.staging.pleo.io/oauth/token/introspect` |
| Production | `https://auth.pleo.io/oauth/token/introspect` |
## Authentication Requirements
The introspection endpoint requires **client authentication** using **HTTP Basic Authentication**:
* `client_id` as the username
* `client_secret` as the password
Only the client that obtained the token may introspect it.
## Making an Introspection Request
Send an HTTP `POST` request with `application/x-www-form-urlencoded` parameters:
| Parameter | Description |
| ----------------- | ---------------------------------------------------------------- |
| `token` | **REQUIRED** – The access token (or refresh token) to introspect |
| `token_type_hint` | OPTIONAL – `access_token` or `refresh_token` |
## Introspection Response
The response is a JSON object containing token metadata.
| Field | Description |
| ----------------------------------- | -------------------------------------- |
| `active` | `true` if the token is currently valid |
| `sub` | Subject the token represents |
| `exp` | Expiration time (UNIX timestamp) |
| `iat` | Issued-at time (UNIX timestamp) |
| `client_id` | Client that requested the token |
| `aud` | Intended audience(s) |
| `iss` | Token issuer |
| `jti` | Token identifier |
| `urn:pleo:params:oauth:subject_urn` | Pleo-specific resource identifier |
Introspection reflects **current server state**. A token may become inactive at any time due to revocation, expiry, or security events.
## Example Request
```http theme={null}
POST /oauth/token/introspect HTTP/1.1
Host: auth.staging.pleo.io
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
token=mF_9.B5f-4.1JqM
```
## Example Response
```json theme={null}
{
"active": true,
"sub": "user_12345",
"exp": 1735689600,
"iat": 1735686000,
"client_id": "client_abc",
"aud": ["pleo-api"],
"iss": "https://auth.pleo.io",
"jti": "f1c2d3"
}
```
## Related Reading
* **[OAuth 2.0 Overview](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview)** - introduction to OAuth 2.0 for Pleo integrations
* **[API Keys Overview](/docs/current/authentication/standalone-api-keys-overview)** – alternative authentication method
* **[Tokens Overview](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview)** – understanding access and refresh tokens
* **[Secure Token Storage](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-secure-token-storage)** – storing credentials securely
* **[Centralised Token Refresh](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-centralised-token-refresh)** – safe token refresh patterns
* **[Race Condition Prevention](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-race-condition-prevention)** – avoid duplicate token refresh attempts
* **[OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)** - Step-by-step guide to configure OAuth 2.0 for your integration
# Tokens Overview
Source: https://developers.pleo.io/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-token-overview
In OAuth 2.0, **access tokens** and **refresh tokens** are used to securely access protected Pleo APIs without exposing user credentials.
This page explains *what tokens are*, how they behave, and the rules integrations must follow when using them. It does not describe implementation details, which are covered in the linked lifecycle guides.
Both access tokens and refresh tokens must be treated as **opaque values**. Integrations must not inspect, decode, or rely on token structure, contents, or format.
## Access Tokens
Access tokens allow your integration to call Pleo APIs on behalf of an authorised user, within the limits of the granted scopes.
* Issued by Pleo’s **Authorisation Server**
* Sent with each request to Pleo **resource servers**
* **Short-lived** and expire after a limited period
The access token lifetime is provided in the `expires_in` field of the token response.
Your integration should proactively refresh access tokens **before** they expire to avoid failed API calls or degraded user experience.
## Refresh Tokens
Refresh tokens allow your integration to obtain new access tokens **without requiring the user to reauthenticate**.
* Longer-lived than access tokens
* Lifetime is **not fixed** and must not be assumed
* Valid for **at least 60 days**, but may expire or be revoked earlier
If an expired refresh token is used, all active refresh tokens for that authorisation are invalidated to prevent replay attacks.
## Token Opacity and Safety
Access tokens and refresh tokens are **opaque strings** issued by Pleo.
Do not parse, decode, or infer meaning from token values. Their format is not guaranteed and may change without notice.
Your integration must store tokens securely and present them back to Pleo APIs **exactly as received**.
## Token Replacement Rules
When refreshing tokens:
* A **new access token** and **new refresh token** are issued
* **All previous access and refresh tokens must be discarded immediately**
* Reusing old tokens may result in authentication failures or revoked sessions
Integrations must always persist and use **only the latest token pair**.
## Common Token Scenarios
### Access Token Expired
* Use the refresh token to obtain a new access token
* Do **not** prompt the user unless refresh fails
### Refresh Token Expired or Revoked
* Restart the OAuth 2.0 authorisation flow
* Prompt the user to reauthenticate and grant consent again
## FAQs
These terms describe different parts of how OAuth 2.0 works in the Pleo platform.
The difference between an **integration**, **application**, and **OAuth 2.0 client** is explained in the [OAuth 2.0 Concepts and Terminology](/docs/current/authentication/oauth/oauth-overview#concepts-and-terminology) section.
## Related Reading
* **[Centralised Token Refresh](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-centralised-token-refresh)**\
Design a single, reliable mechanism for refreshing tokens across your system.
* **[Race Condition Prevention](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-race-condition-prevention)**\
Prevent duplicate refresh requests when multiple processes detect expiry simultaneously.
* **[Secure Token Storage](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-secure-token-storage)**\
Store access and refresh tokens safely in backend and frontend environments.
* **[Handling Refresh Token Expiry or Revocation](/docs/current/integration-design/auth/oauth/token-lifecycle/integration-design-auth-oauth-refresh-token-expiry-revocation)**\
Recover gracefully when refresh tokens are no longer valid.
# Accounting Periods
Source: https://developers.pleo.io/docs/current/integration-design/exports/integration-design-exports-accounting-periods
This page describes how integrations must implement accounting period assignment when exporting expenses from Pleo.
Accounting periods must be assigned after Export Items are validated and before entries are posted to the Accounting System.
## Implementation
See the corresponding how-to article for API usage and step-by-step instructions:
* [How to Assign Accounting Periods](/docs/current/how-tos/accounting-integrations/how-to-assign-accounting-periods-for-as-erp-processing)
## Implementation Requirements
### 1. Determine Expense Date (source date)
Use the expense transaction date provided in the Export Item as the source date:
* Expense date = `date` of the Export Item
This is the baseline date the expense relates to, as provided by Pleo via Export API.
### 2. Determine posting date (date sent to the Accounting System)
The posting date is the date your integration will send to the Accounting System when creating the corresponding entry:
* **Posting date** = the date you post with (often starts as `date`)
In most cases:
```pseudo theme={null}
postingDate = exportItem.date
```
### 3. Closed period handling (integration configuration)
The Export API does **not** provide a closed-period adjustment setting.
If your integration needs to support posting expenses that fall into closed periods, you must implement this as an **integration configuration**.
Behaviour:
1. Attempt to post using: `postingDate = exportItem.date`
2. If the Accounting System **rejects** the posting because the period is closed, apply the configured strategy.
#### Recommended configuration options
* If **Closed period date adjustment** is **enabled** in the integration:
* When the Accounting System rejects the posting date due to a closed period, apply the configured adjustment strategy. Common strategies include:
* Set `postingDate = exportDate` (i.e., “today”)
* Move `postingDate` to the **next open period** (per Accounting System rules)
* If **Closed period date adjustment** is **disabled**:
* Attempt to post using the original Export Item transaction date (`postingDate = exportItem.date`)
* If the Accounting System rejects closed periods, mark the Export Item as failed with a clear error (e.g., “Closed accounting period”)
### 4. Assign accounting period (Accounting System responsibility)
The integration must:
* map the **posting date** to the correct Accounting System period
* rely on Accounting System logic when periods are custom or fiscal-based
```pseudo theme={null}
period = accountingSystem.resolvePeriod(postingDate)
```
### 5. Custom accounting periods (non-calendar fiscal structures)
If the Accounting System uses non-calendar periods:
* use Accounting System APIs and/or configuration to resolve the correct period for `postingDate`
* ensure exported entries align with the system’s fiscal structure (do not assume Jan–Dec)
### 6. Optional: Service period (for accrual/amortisation)
If `exportItem.servicePeriod` is present, it indicates the time range the cost relates to (common for invoices). It can be used by the Accounting System to allocate costs across periods (accruals), but it does **not** change closed-period handling by itself.
```json theme={null}
"servicePeriod": {
"from": "YYYY-MM-DD",
"to": "YYYY-MM-DD"
}
```
## Processing Order
Accounting period assignment occurs within the export workflow:
## Upstream Dependencies
* Export Job has been started (status = in\_progress)
* Fetch Export Item Data (data layer)
* Bookkeeping method resolved
* **Accounts Mapping** – determines which GL accounts are debited and credited
* **Data Mapping** – ensures amounts, dates, dimensions, identifiers, and VAT/tax information are recorded correctly
* **Attachment Handling** – links receipts and supporting documentation
## Downstream Dependencies
* **Posting Behaviour** – determines whether entries are created as drafts or finalised
* **Export Item status update** – updates outcome of Export Item (`successful` or `failed`)
* **Export Job status update** – updates outcome of Export Job (`completed`, `completed_with_errors` or `failed`)
***
## What Comes Next?
* [Posting Behaviour](/docs/current/integration-design/exports/integration-design-exports-posting-behaviour)
***
## Related Reading
* [How to Assign Accounting Periods](/docs/current/how-tos/accounting-integrations/how-to-assign-accounting-periods-for-as-erp-processing)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Accounts Mapping
Source: https://developers.pleo.io/docs/current/integration-design/exports/integration-design-exports-accounts-mapping
Accounts Mapping determines which **General Ledger (GL) accounts** are used to record debit and credit entries for each Export Item.
All exported expenses follow **double-entry bookkeeping**, meaning every transaction must have both:
* a debit account
* a credit (counter) account
Accounts Mapping occurs **after**:
* Export Items are fetched
* [Bookkeeping method](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-resolution) has been resolved
## Implementation
See the corresponding how-to article for API usage and step-by-step instructions:
* [How to Apply Accounts Mapping for Export Items](/docs/current/how-tos/accounting-integrations/how-to-determine-accounts-mapping-for-as-erp-processing)
## Mapping Inputs
Accounts Mapping depends on:
* `data[].type` and `data[].subType` (e.g., `card_purchase`, `invoice`, `wallet_topup`)
* Expense category (Expense GL Account)
* Bookkeeping method:
* `journal`
* `accounts_payable`
## Required Account Configuration
The integration must obtain the following GL accounts from user configuration in the Accounting System:
* Pleo Wallet Account
* Pleo Out-of-Pocket Account
* Pleo Contra Account
* Accounts Payable Account (fallback)
These accounts define the base posting structure used across all Export Items.
## Vendor-Specific Accounts Payable
### When Accounts Payable Applies
If:
```json theme={null}
data[].bookkeeping.method = accounts_payable
```
the counter account is determined using vendor resolution logic.
### Vendor Resolution Logic
1. If `data[].vendor` exists:
* Match vendor in the Accounting System using:
* `vendor.code` (preferred)
* or `supplier.code` (legacy support)
2. If a match is found:
* Use vendor-specific Accounts Payable account
3. If no match or vendor is missing:
* Use fallback Accounts Payable account from configuration
#### Example
* Vendor = `ACME Ltd` → matched → `2901-AP-ACME`
* Vendor not found → `2000-AP-Default`
Some older integrations may still provide `supplier.code` for vendor matching. Use only when [applicable](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-accounts-payable#legacy-support).
## Expense Booking Logic
### Scope
This section defines how debit and credit accounts are assigned once:
* bookkeeping method is resolved
* (for Accounts Payable) vendor resolution is completed
### Line-Level Expense Account
Each Export Item line provides the expense (debit) account:
```json theme={null}
data[].accountingEntryLines[].account
```
### Fields
* **id** – internal Pleo account ID
* **name** – category name
* **code** – ERP-facing GL account code
* **identifier** – ERP-facing identifier
### Counter Account Sources
The counter account is determined as follows:
#### Journal Entries
If:
```pseudo theme={null}
data[].bookkeeping.method = journal
```
Counter account is determined using:
* `data[].type` + `data[].subType`
* Expense Type Mapping rules
* `data[].contraAccount` (when provided)
* configured accounts:
* Wallet account
* Out-of-pocket account
* Contra account
#### Accounts Payable Entries
If:
```pseudo theme={null}
data[].bookkeeping.method = accounts_payable
```
Counter account is:
* vendor-specific AP account (if resolved)
* otherwise fallback AP account
## Mapping Rule Summary
1. Identify bookkeeping method
2. Set expense (debit) account from:
* `data[].accountingEntryLines[].account`
3. Determine counter account:
* journal
* Based on Expense Type Mapping rules
* `data[].type` + `data[].subType`
* `data[].contraAccount` (if applicable)
* configured wallet / out-of-pocket / contra accounts
* Accounts Payable
* Vendor-specific Accounts Payable account if matched
* Otherwise fallback Accounts Payable account
### Example Mapping Table
| Export Item Type | Bookkeeping Method | Debit Account (Expense) | Credit Account (Counter) |
| ---------------- | ------------------ | --------------------------------------- | ------------------------------------------ |
| `card_purchase` | `journal` | `data[].accountingEntryLines[].account` | `data[].contraAccount` (wallet/bank) |
| `invoice` | `accounts_payable` | `data[].accountingEntryLines[].account` | Vendor AP account or fallback |
| `wallet_topup` | `journal` | Wallet account | Contra account (direction depends on flow) |
* Each transaction always has one expense (debit) account and one counter (credit) account
* Direction may invert for refunds or reversals as defined by mapping rules
### Expense Type Mapping Dependency
Accounts Mapping relies on Expense Type Mapping, which defines:
* how `type` and `subType` determine accounting treatment
* how counter accounts are selected for journal entries
## Upstream Dependencies
* Export Job has been started (status = in\_progress)
* Fetch Export Item Data (data layer)
* Bookkeeping method resolved
## Downstream Dependencies
* **Data Mapping** – ensures amounts, dates, dimensions, identifiers, and VAT/tax information are recorded correctly
* **Attachment Handling** – links receipts and supporting documentation
* **Accounting Periods** – assigns entries to the correct accounting period
* **Posting Behaviour** – determines whether entries are created as drafts or finalised
* **Export Item status update** – updates outcome of Export Item (`successful` or `failed`)
* **Export Job status update** – updates outcome of Export Job (`completed`, `completed_with_errors` or `failed`)
***
## What Comes Next?
* [Data Mapping](/docs/current/integration-design/exports/integration-design-exports-data-mapping)
***
## Related Reading
* [How to Apply Accounts Mapping for Export Items](/docs/current/how-tos/accounting-integrations/how-to-determine-accounts-mapping-for-as-erp-processing)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Attachment Handling
Source: https://developers.pleo.io/docs/current/integration-design/exports/integration-design-exports-attachment-handling
Attachment handling defines **how integrations must transfer and associate files**
from Pleo Export Items to entries created in the Accounting System.
Platform capabilities and attachment schema are described in: [Platform: Attachments](/docs/current/platform/exports/attachments)
## Implementation
See the corresponding how-to article for API usage and step-by-step instructions:
* [How to Transfer Attachments](/docs/current/how-tos/accounting-integrations/how-to-transfer-attachments-for-as-erp-processing)
## When Attachment Handling Occurs
Attachments must be processed:
1. After Export Items are fetched and validated
2. After bookkeeping method resolution
3. Before posting entries to the Accounting System
## Processing Rules
### 1. Download Attachments
For each Export Item:
* Iterate through `data[].files[]`
* Download each attachment using `files.url`
* Preserve original file format and filename when possible
### 2. Associate Attachments
Attachments must be linked to:
* the Journal Entry, or
* the Accounts Payable invoice/payment
created for the same Export Item.
The integration must ensure attachments remain traceable to the originating expense.
### 3. Multiple Attachments
If multiple attachments exist:
* Upload all files when the Accounting System supports multiple attachments.
If the Accounting System supports only one attachment:
* Combine files into a single document before upload.
Recommended approaches:
* PDF merge (preferred)
* ZIP archive (fallback)
### 4. Failure Handling
Attachment failures must not silently discard files.
Recommended behaviour:
* Retry download on transient failure
* Log failed attachments
* Optionally allow posting without attachment only if accounting entry succeeds
### 5. Idempotency
Integrations should avoid duplicate uploads when exports are retried.
Recommended strategies:
* store processed attachment IDs, or
* check existing attachments before upload.
## Edge Cases
| Scenario | Expected Behaviour |
| -------------------------------------- | --------------------------- |
| No attachments | Continue export normally |
| Attachment download fails | Retry and log error |
| Unsupported file type in AS | Convert or store externally |
| Multiple attachments but AS allows one | Merge files |
## Upstream Dependencies
* Export Job has been started (status = in\_progress)
* Fetch Export Item Data (data layer)
* Bookkeeping method resolved
* **Accounts Mapping** – determines which GL accounts are debited and credited
* **Data Mapping** – ensures amounts, dates, dimensions, identifiers, and VAT/tax information are recorded correctly
## Downstream Dependencies
* **Accounting Periods** – assigns entries to the correct accounting period
* **Posting Behaviour** – determines whether entries are created as drafts or finalised
* **Export Item status update** – updates outcome of Export Item (`successful` or `failed`)
* **Export Job status update** – updates outcome of Export Job (`completed`, `completed_with_errors` or `failed`)
***
## What Comes Next?
* [Accounting Periods](/docs/current/integration-design/exports/integration-design-exports-accounting-periods)
***
## Related Reading
* [How to Transfer Attachments](/docs/current/how-tos/accounting-integrations/how-to-transfer-attachments-for-as-erp-processing)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Accounts Payable Handling
Source: https://developers.pleo.io/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-accounts-payable
When `bookkeeping.method` is `accounts_payable`, Export Items must be recorded using Accounts Payable bookkeeping.
Accounts Payable bookkeeping separates **vendor liability** (invoice) from **vendor settlement** (payment). These events may occur at different times and may originate from different Export Jobs.
This page defines how integrations must construct invoice entries, payment entries, and reconciliation behaviour once the resolved bookkeeping method for an Export Item is `accounts_payable`.
## Implementation
See the corresponding how-to article for API usage and step-by-step instructions:
* [How to Determine the Bookkeeping Method](/docs/current/how-tos/accounting-integrations/how-to-determine-the-bookkeeping-method-for-as-erp-processing)
## Processing Model
Accounts Payable handling follows this processing order:
1. Vendor Resolution
2. Invoice and Payment Recording
3. Expense Type Handling
4. Reconciliation
5. Special Cases
Each Export Item must pass through these steps during processing.
```
{
"data": [
# [other Export Items omitted for brevity]
{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"accountingEntryId": "98441fe2-6d31-4d52-8569-687de8368cb2",
"type": "invoice",
"subType": null,
"date": "2025-04-18T00:00:00Z",
"amount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 130000
},
"inWalletCurrency": {
"currency": "GBP",
"value": 130000
}
},
"note": "This was approved before the event.",
"files": [
{
"url": "https://pleo-invoices.../12abc3d4-e567-890e-1234-abc56e78fabc/CR4VBJ7UUGK00/original.pdf..."
}
],
"supplier": {
"code": null,
"name": "TestSupplier123",
"categoryCode": null,
"country": "GB",
"account": null,
"taxIdentifier": "12345"
},
"user": {
"id": "f1b5d950-1dbd-4493-8e8c-59fcfe13964f",
"name": "Natalie White",
"code": null
},
"team": null,
"accountingEntryLines": [
{
"accountingEntryLineId": "98441fe2-6d31-4d52-8569-687de8368cb2",
"lineAmount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 130000
},
"inWalletCurrency": {
"currency": "GBP",
"value": 130000
}
},
"netAmount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 108333
},
"inWalletCurrency": {
"currency": "GBP",
"value": 108333
}
},
"account": {
"id": "eef29cf3-c6aa-45a8-b316-ae5b61d3a497",
"code": "4650000",
"name": "Marketing",
"identifier": "4650000"
},
"tax": {
"id": "b7440cfa-080e-42f2-a267-feb065ac98c9",
"code": "0002",
"type": "inclusive",
"amount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 21667
},
"inWalletCurrency": {
"currency": "GBP",
"value": 21667
}
},
"rate": 0.20
},
"tags": [
{
"id": "4818af60-c790-4208-930a-87db98108193",
"groupCode": "Project A",
"code": "004"
}
]
}
],
"additionalInformation": {
"reconciliationId": "0",
"reconciledEntries": null,
"attendees": [],
"invoiceInformation": {
"invoiceNumber": "WZM259435",
"status": "ready",
"invoiceDate": "2025-04-18T00:00:00Z",
"dueDate": "2025-04-30T00:00:00Z",
"paymentDate": null,
"supplierBankAccount": {
"bankName": "NATIONAL WESTMINSTER BANK PLC",
"bankCode": "123456",
"accountNumber": "09876543",
"country": "GB",
"bic": null,
"iban": null
}
}
},
"bookkeeping": {
"method": "accounts_payable"
},
"vendor": {
"id": "22e1f2c9-1360-4291-ab41-6b23dcea8888",
"name": "TestVendor",
"code": "acc1234",
"externalId": "ext12345",
"registrationNumber": "reg001234",
"taxRegistrationNumber": "taxreg1234",
"country": "UK",
"defaultCurrency": "GBP"
},
"contraAccount": null,
"_links": {
"web": {
"exportItem": "https://app.staging.pleo.io/export/export-item/98441fe2-6d31-4d52-8569-687de8368cb2"
}
},
"servicePeriod": {
"from": "2025-04-18T00:00:00Z",
"to": "2025-04-18T00:00:00Z"
}
}
],
"pagination": {
"hasPreviousPage": false,
"hasNextPage": false,
"currentRequestPagination": {
"sortingKeys": [],
"sortingOrder": [],
"parameters": {
"job_id": [
"c62931c9-ecd4-4a6f-9f43-f759ac297707"
]
}
},
"startCursor": "AAAAAADJ5MVAOIR2ZFYA=ZOA73ZCZDVDJNGJSQHCX2LCGPY",
"endCursor": "AAAAAADJ5MVAOIRZQFIA=GBA43XWBQRCCLFTOWOQKVJUAUU",
"total": 4
}
}
```
## 1. Vendor Resolution
### Detection
`bookkeeping.method = accounts_payable`
### Rule
A vendor **must** be resolved before creating invoice entries.
The integration must attempt vendor matching using the following priority order:
1. **`vendor.code`**
2. **`vendor.account`**
3. **`vendor.taxIdentifier`**
If a `vendor` object exists, the `supplier` object must be ignored.
### Failure Behaviour
If vendor matching fails:
* The Export Item must not be recorded
* The Export Item must fail in Pleo
Failure reporting:
* **failureReasonType**
* `vendor_unknown` (Export V3)
* `accounting_system_validation_failure` (Export V2)
* **failureReasonMessage**
Selected vendor cannot be processed because it does not exist or is currently blocked. Please verify the vendor's status in the accounting system and try again.
### Legacy Support
If no `bookkeeping` object is present but a `supplier` object exists, attempt
vendor resolution using the supplier object's identifiers.
Use the following priority order to find a matching vendor in your AS:
| Priority | Supplier Field | Match Against in AS |
| -------- | ------------------------ | ------------------------------------------------ |
| 1 | `supplier.code` | Vendor ID |
| 2 | `supplier.account` | Vendor account number |
| 3 | `supplier.taxIdentifier` | Vendor tax identifier or tax registration number |
Work through each field in order, stopping as soon as a match is found.
If no match is found after all three, fail the Export Item using the same
failure behaviour as standard vendor resolution.
## 2. Invoice and Payment Model
### Detection
`bookkeeping.method = accounts_payable`
### Rule
The integration must record:
* Vendor liability as an **invoice object**
* Vendor settlement as a **payment entry**
Payment recording may occur by:
* Marking the invoice as paid, and/or
* Creating a separate payment entry
Implementation depends on Accounting System capabilities.
### Behaviour Notes
Invoice and payment entries may be recorded at different times and may originate from separate Export Jobs.
## 3. Expense Type Handling
### Detection
After vendor resolution succeeds.
### Rule
Export Item type determines whether an invoice, payment, or both must be recorded.
### card\_invoice, card\_purchase, fee
* Record an invoice entry
* Record a payment entry
* Reconcile invoice and payment
* Set invoice status to `paid`
### invoice
* Record an invoice entry only
* Set invoice status to `unpaid`
* Reconcile once payment is recorded
### invoice\_payment (`invoiceInformation.status = paid`)
* Record a payment entry only
* Reconcile using `reconciliationId`
* Set invoice status to `paid`
### invoice\_payment (`invoiceInformation.status = failed_payment`)
If the bank rejects the payment and funds return to the Pleo wallet:
* Record a return vendor payment
* Set invoice status to `unpaid`
## 4. Recording and Reconciliation
### Detection
Both invoice and payment entries exist.
### Rule
The integration must reconcile invoice and payment entries.
Reconciliation may occur across Export Jobs.
After reconciliation:
* Invoice payment status must be set to `paid`.
## 5. Special Cases
### Refunds and Chargebacks
#### Detection
Export Item type is `refund` or `chargeback`.
#### Rule
If supported by the Accounting System:
* Record a **credit note** instead of an invoice
* Record the corresponding refund payment
* Reconcile credit note and payment
* Set credit note status to `paid`
### Split Expenses
#### Detection
If the Export Item contains more than one split line (`accountingEntryLines.length > 1`), it must be treated as a split expense.
#### Rule
* Record each split line as a separate line on the invoice
* Record a single payment line representing the total amount across all splits
* Reconcile invoice lines with the payment as usual
#### Example
Create the following in your Accounting System if the expense has 2 splits:
* Line 1 (API): £120 → create 1 invoice line
* Line 2 (API): £80 → create 1 invoice line
* Line 3 (calculated total): £200 → create 1 payment line
## Upstream Dependencies
* Export Job has been started (status = `in_progress`)
* Fetch Export Item Data (data layer)
* Bookkeeping method resolved
## Downstream Dependencies
Accounts Payable entries rely on several platform concepts to be **complete and reconcilable**:
* **Accounts Mapping** – determines which GL accounts are debited and credited
* **Data Mapping** – ensures amounts, dates, dimensions, identifiers, and VAT/tax information are recorded correctly
* **Attachment Handling** – links receipts and supporting documentation
* **Accounting Periods** – assigns entries to the correct accounting period
* **Posting Behaviour** – determines whether entries are created as drafts or finalised
* **Export Item status update** – updates outcome of Export Item (`successful` or `failed`)
* **Export Job status update** – updates outcome of Export Job (`completed`, `completed_with_errors` or `failed`)
***
## What Comes Next?
* [Accounts Mapping](/docs/current/integration-design/exports/integration-design-exports-accounts-mapping)
***
## Related Reading
* [How to Determine the Bookkeeping Method](/docs/current/how-tos/accounting-integrations/how-to-determine-the-bookkeeping-method-for-as-erp-processing)
* [Journal Entry Handling](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-journal-entry)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
* [Paying a Supplier Invoice Using Pleo Web App](https://help.pleo.io/en/support/solutions/articles/103000285539-paying-a-supplier-invoice)
***
# Journal Entry Handling
Source: https://developers.pleo.io/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-journal-entry
For Export Items where the resolved bookkeeping method is `journal`, the integration must record the expense as **journal entries** in the Accounting System.
This page defines how journal entries must be constructed during export processing, including how debit and credit lines are derived from the Export Item payload.
Journal entry creation operates after bookkeeping method resolution (see [Determine Bookkeeping Method](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-resolution)) and applies Accounts Mapping, Data Mapping, and Posting Behaviour rules described elsewhere in this documentation set.
## Implementation
See the corresponding how-to article for API usage and step-by-step instructions:
* [How to Determine the Bookkeeping Method](/docs/current/how-tos/accounting-integrations/how-to-determine-the-bookkeeping-method-for-as-erp-processing)
## Journal Entry Construction Modes
An integration may need to construct journal entries in one of three modes depending on configuration and accounting system constraints:
1. **Single journal entry per Export Item (default)**
2. **Split line journal entry**
3. **Single journal entry per Export Job (optional aggregation)**
The integration determines which mode applies at runtime.
```json theme={null}
{
"companyId": "12abc3d4-e567-890e-1234-abc56e78fabc",
"accountingEntryId": "59540ed2-0d68-4e36-9e31-58223975d9e9",
"type": "card_purchase",
"subType": null,
"date": "2025-12-10T15:46:34Z",
"amount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"note": "Printer ink",
"files": [
{
"url": "",
"type": "image/jpeg",
"size": 13010
}
],
"supplier": {
"code": "1340472473",
"name": "Target",
"categoryCode": "1000",
"country": "GB",
"account": null,
"taxIdentifier": null
},
"user": {
"id": "6b71f6bd-e83d-4d49-88ee-2b8cda2d57cf",
"name": "Luke Richardson",
"code": null
},
"team": {
"id": "747aaf60-56c6-4b46-ad92-f8a0cb59cf8b",
"code": "5678",
"name": "Engineering"
},
"accountingEntryLines": [
{
"accountingEntryLineId": "0c76ea71-aaaa-4ece-bb68-e1166ccaea04",
"lineAmount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"netAmount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 6366
},
"inWalletCurrency": {
"currency": "GBP",
"value": 6366
}
},
"account": {
"id": "7966c3ba-e4de-4574-8604-6cfa48d62cc8",
"code": "6990000",
"name": "Printing & Stationary",
"identifier": "6990000"
},
"tax": {
"id": "997d8526-5872-484d-ba07-c7a07e08e555",
"code": "0001",
"type": "inclusive",
"amount": {
"inSupplierCurrency": {
"currency": "GBP",
"value": 0
},
"inWalletCurrency": {
"currency": "GBP",
"value": 0
}
},
"rate": 0.00
},
"tags": []
}
],
"additionalInformation": {
"reconciliationId": "2500001",
"reconciledEntries": null,
"attendees": [],
"invoiceInformation": null
},
"bookkeeping": {
"method": "journal"
},
"vendor": {
"id": "22e1f2c9-1360-4291-ab41-6b23dcea8888",
"name": "TestVendor",
"code": "acc1234",
"externalId": "ext12345",
"registrationNumber": "reg001234",
"taxRegistrationNumber": "taxreg1234",
"country": "UK",
"defaultCurrency": "GBP"
},
"contraAccount": {
"id": "993d664c-9b7c-4efc-a677-510e69200857",
"code": "0876000",
"name": "0876000_ChartAccounts",
"identifier": "0876000"
},
"_links": {
"web": {
"exportItem": "https://app.staging.pleo.io/export/export-item/0c76ea71-aaaa-4ece-bb68-e1166ccaea04"
}
},
"servicePeriod": null
},
# [other Export Items omitted for brevity]
# [Pagination omitted for brevity]
```
## 1. Single Journal Entry per Export Item (Default)
### Detection
If `accountingEntryLines` contains a single entry, the Export Item must be recorded as a single journal entry.
### Rule
The integration must create one journal entry representing the full expense.
For each journal entry:
* The full expense amount must be represented
* Debit, credit, and tax data must be recorded according to the Accounting System’s structure
* Attachments (e.g., receipts) should be linked if supported
Some Accounting Systems allow debit, credit, and tax within a single line, while others internally split postings. The integration should follow the Accounting System’s required structure.
## 2. Split Line Handling
### Detection
If `accountingEntryLines` contains more than one entry, the Export Item must be treated as a split line entry.
### Rule
Each entry in `accountingEntryLines` represents a debit posting.
The API provides individual split amounts, each with its own account and tax code.
The payload does **not** contain a credit line.
The integration is responsible for:
1. Creating one debit posting per `accountingEntryLines` entry
2. Calculating the credit total as the sum of all `lineAmount.inWalletCurrency.value` values across the lines
The calculated total must match the top-level amount: `amount.inWalletCurrency.value`
### Example
Create the following in the Accounting System if two split lines exist:
* Line 1 (API): £120 → create 1 debit line
* Line 2 (API): £80 → create 1 debit line
* Line 3 (integration calculated or top-level amount): £200 → create 1 credit line
## 3. One Journal Entry per Export Job (Optional)
An integration may optionally support aggregating multiple Export Items into a single journal entry per Export Job.
Aggregation changes *when* journal entries are created, not how debit and credit lines are constructed.
### Detection
This behaviour is decided entirely by the integration implementation (your code) and is **not** a configuration setting in Pleo or dynamically signalled by the Export Job payload.
Only implement this if the target Accounting System explicitly supports it and the integration is configured accordingly. This is typically used for high-volume Export Jobs.
### Rule
When aggregation is enabled, the integration:
1. Fetches all Export Items for the job
2. Identifies items where `bookkeeping.method` is `journal` (or `none`)
3. Creates a single journal entry instead of one per Export Item
4. Sets the journal entry date to the Export Job date (`createdAt` on the Export Job)
Split line rules still apply across aggregated items:
* Each item's debit lines are represented individually
* A single credit line represents the total amount across all aggregated items
### Example
Create the following in the Accounting System if three Export Items exist:
* Line 1: £100 (no splits) → 1 debit line
* Lines 2 & 3: £200 (2 splits) → 2 debit lines (£120 and £80)
* Line 4: £50 (no splits) → 1 debit line
* Line 5: £350 → 1 credit line
The resulting journal entry contains four debit lines and one credit line for £350.
## Upstream Dependencies
* Export Job has been started (status = in\_progress)
* Fetch Export Item Data (data layer)
* Bookkeeping method resolved
## Downstream Dependencies
Journal entries rely on several platform concepts to be **complete and reconcilable**:
* **Accounts Mapping** – determines which GL accounts are debited and credited
* **Data Mapping** – ensures amounts, dates, dimensions, identifiers, and VAT/tax information are recorded correctly
* **Attachment Handling** – links receipts and supporting documentation
* **Accounting Periods** – assigns entries to the correct accounting period
* **Posting Behaviour** – determines whether entries are created as drafts or finalised
* **Export Item status update** – updates outcome of Export Item (`successful` or `failed`)
* **Export Job status update** – updates outcome of Export Job (`completed`, `completed_with_errors` or `failed`)
***
## What Comes Next?
* [Accounts Mapping](/docs/current/integration-design/exports/integration-design-exports-accounts-mapping)
***
## Related Reading
* [How to Determine the Bookkeeping Method](/docs/current/how-tos/accounting-integrations/how-to-determine-the-bookkeeping-method-for-as-erp-processing)
* [Accounts Payable Handling](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-accounts-payable)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Determine Bookkeeping Method
Source: https://developers.pleo.io/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-resolution
Each Export Item in Pleo carries a `bookkeeping.method` datapoint, which defines how the expense should be recorded in the Accounting System. The integration must read this value and branch processing accordingly.
This step ensures that the expense is recorded in line with the selected bookkeeping method while respecting the default handling rules for non-payable transactions.
## Implementation
See the corresponding how-to article for API usage and step-by-step instructions:
* [How to Determine the Bookkeeping Method](/docs/current/how-tos/accounting-integrations/how-to-determine-the-bookkeeping-method-for-as-erp-processing)
## Purpose
Before any accounting entries are created, each Export Item must have a **resolved bookkeeping method**. This resolution step **ensures that downstream processes are applied correctly**. It determines:
* How the expense is represented in the Accounting System
* Which downstream processes apply, including Accounts Mapping, Data Mapping, Invoice/Payment creation, and Posting Behaviour
Resolution ensures **consistency with platform rules** and supports correct handling of default transaction types.
## Resolution Logic
The integration reads the `bookkeeping.method` datapoint on the Export Item and applies the following logic:
| Bookkeeping Method | Action | Notes |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `journal` | Record the expense as a [Journal Entry](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-journal-entry) | Standard double-entry ledger posting |
| `accounts_payable` | Record the expense using the [Accounts Payable](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-accounts-payable) workflow | Generates invoice and payment entries; supports credit notes and split expenses |
| `none` | Record the expense as a Journal Entry | Represents non-payable balance adjustments such as wallet loads, wallet unloads, and balance amendments; does not require Accounts Payable handling |
| `null` | See [legacy support](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-accounts-payable#legacy-support) | |
### Notes on Default Behaviour
* Certain transaction types are always recorded as Journal Entries regardless of explicit configuration:
* Wallet loads
* Wallet unloads
* Balance amendments
* These default rules ensure consistency with Platform Capabilities, where these transactions do **not** represent vendor liabilities.
## Expected Outcome
After resolution:
* Each Export Item is tagged with a **resolved bookkeeping method**.
* Downstream processes rely on this resolution to correctly apply **Accounts Mapping**, **Data Mapping**, **Invoice/Payment creation**, **Attachment Handling**, **Accounting Periods**, and **Posting Behaviour**.
***
## What Comes Next?
* [Journal Entry Handling](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-journal-entry)
***
## Related Reading
* [How to Determine the Bookkeeping Method](/docs/current/how-tos/accounting-integrations/how-to-determine-the-bookkeeping-method-for-as-erp-processing)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Data Mapping
Source: https://developers.pleo.io/docs/current/integration-design/exports/integration-design-exports-data-mapping
Data Mapping defines how Export Item data must be translated into Accounting System (AS) journal or invoice fields.
Mappings are applied **after**:
* Export Items are fetched
* Pre-validation has passed
* Accounts Mapping has been resolved
## Implementation
See the corresponding how-to article for API usage and step-by-step instructions:
* [How to Apply Data Mapping](/docs/current/how-tos/accounting-integrations/how-to-apply-data-mapping-for-as-erp-processing)
## Mapping Order (Implementation Flow)
Integrations should apply mappings in the following order:
1. **Mandatory datapoints** (always required)
2. **Invoice datapoints** (invoice-related items only)
3. **Optional datapoints** (system-dependent)
4. **Apply VAT handling rules**
Mapped datapoints are then used to construct journal entries or invoices in the Accounting System according to the selected bookkeeping method.
## Mandatory Datapoints
These datapoints must be recorded for **all exported expenses**, regardless of bookkeeping method.
* `vendor` is the canonical field used for Accounts Payable bookkeeping and vendor matching.
* `supplier` is a legacy field (e.g. `supplier.code`) retained for backwards compatibility.
* `supplier.name` may still be used as a descriptive merchant name but should not be relied on for accounting logic.
Mandatory datapoints represent the minimum information required for reconciliation and accounting accuracy.
### Required Data
* Expense GL Account (Category)
* Expense amount (wallet currency, derived from line amounts)
* Accounting Dimensions (Tags)
* Tax Code
* Expense Date
* Reconciliation ID
* Reconciled Entries (reimbursements)
* Consolidated Description
* Vendor (Accounts Payable only)
* Attendees Information
## Mandatory Datapoint Mapping (Pleo → AS)
These fields are often mapped to shared reference or memo fields in the Accounting System.
To avoid losing existing accounting data, consider whether to:
* append rather than overwrite existing values
* map to dedicated/custom fields where available
### Entry-Level Mapping
| Accounting Concept | Pleo API Field | Notes |
| ------------------ | -------------------------------------------------------- | ------------------------------------------------------------------------------ |
| Expense Date | data\[].date | Transaction date |
| Reconciliation ID | data\[].additionalInformation.reconciliationId | Map to external reference or description field (≥50 chars) |
| Reconciled Entries | data\[].additionalInformation.reconciledEntries | Used for reimbursement linking; map to reference/description field (≥50 chars) |
| Attendees | data\[].additionalInformation.attendees | Map to description/comment field (≥100 chars) |
| Description / Note | data\[].supplier.name + data\[].user.name + data\[].note | Concatenated description (use initials if length constrained) |
### Accounts Payable Only (Entry-Level)
| Accounting Concept | Pleo API Field | Notes |
| ------------------ | ------------------- | ------------------------------- |
| Vendor Name | data\[].vendor.name | Required for `accounts_payable` |
| Vendor Code | data\[].vendor.code | Used for vendor matching in AS |
### Line-Level Mapping
| Accounting Concept | Pleo API Field | Notes |
| --------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------ |
| Expense GL Account | data\[].accountingEntryLines\[].account.code | Expense/category account |
| Expense Amount (Wallet, per line) | data\[].accountingEntryLines\[].lineAmount.inWalletCurrency.value | Sum lines if AS requires total |
| Currency Code | data\[].accountingEntryLines\[].lineAmount.inWalletCurrency.currency | |
| Dimension | data\[].accountingEntryLines\[].tags.groupCode | e.g. Project, Cost Center |
| Dimension Value | data\[].accountingEntryLines\[].tags.code | |
| Tax Code | data\[].accountingEntryLines\[].tax.code | |
| Tax Rate | data\[].accountingEntryLines\[].tax.rate | Only if required (usually derived from Tax Code in AS) |
## Invoice Datapoint Mapping
Apply only when `data[].type = invoice` or `invoice_payment`.
Invoice datapoints extend mandatory mappings with supplier invoice information.
### Rules
* `invoice` → use Supplier currency
* `invoice_payment` → use Wallet currency
* Provide exchange rate if required by the Accounting System
| Accounting Concept | Pleo API Field | Notes |
| ------------------ | ---------------------------------------------------------------------- | ----------------------------- |
| Invoice Due Date | data\[].additionalInformation.invoiceInformation.dueDate | Dedicated due date field |
| Invoice Date | data\[].additionalInformation.invoiceInformation.invoiceDate | |
| Invoice Number | data\[].additionalInformation.invoiceInformation.invoiceNumber | |
| Payment Date | data\[].additionalInformation.invoiceInformation.paymentDate | For payment entries |
| Supplier Amount | data\[].accountingEntryLines\[].lineAmount.inSupplierCurrency.value | Line-level |
| Supplier Currency | data\[].accountingEntryLines\[].lineAmount.inSupplierCurrency.currency | Use AS foreign currency field |
## Optional Datapoints
Optional datapoints improve reporting but must never replace mandatory data.
Optional mappings must not overwrite mandatory fields.
### Examples
* Internal document number
* Net and tax amounts
* Team dimensions
* Export job date
## Optional Datapoint Mapping
| Accounting Concept | Pleo API Field | Notes |
| ------------------------ | -------------------------------------------------------------------- | ----------------------------------------------------------- |
| Net Amount | data\[].accountingEntryLines\[].netAmount.inWalletCurrency.value | Only if AS requires net values for tax calculation |
| Net Currency | data\[].accountingEntryLines\[].netAmount.inWalletCurrency.currency | |
| Tax Amount | data\[].accountingEntryLines\[].tax.amount.inWalletCurrency.value | Only if AS cannot calculate VAT |
| Tax Currency | data\[].accountingEntryLines\[].tax.amount.inWalletCurrency.currency | |
| Team (Dimension) | data\[].team.code | Map to configured dimension value |
| Internal Document Number | (AS generated) | Do not overwrite; allow AS to assign |
| Export Date | (integration-defined) | Use instead of expense date when posting aggregated entries |
## VAT Handling
VAT handling depends on Accounting System capabilities.
### Default Behaviour
The Accounting System calculates VAT using:
* line amounts
* tax codes
### When to Send VAT Amounts
Only send VAT values if the Accounting System:
* cannot calculate tax automatically, or
* requires explicit VAT posting
* If AS calculates VAT → **do NOT** send VAT amount
* If AS cannot calculate VAT → **send VAT from Pleo**
## Upstream Dependencies
* Export Job has been started (status = in\_progress)
* Fetch Export Item Data (data layer)
* Bookkeeping method resolved
* Accounts Mapping – determines which GL accounts are debited and credited
## Downstream Dependencies
* Attachment Handling – links receipts and supporting documentation
* Accounting Periods – assigns entries to the correct accounting period
* Posting Behaviour – determines whether entries are created as drafts or finalised
* Export Item status update – updates outcome of Export Item (`successful` or `failed`)
* Export Job status update – updates outcome of Export Job (`completed`, `completed_with_errors` or `failed`)
***
## What Comes Next?
* [Attachment Handling](/docs/current/integration-design/exports/integration-design-exports-attachment-handling)
***
## Related Reading
* [How to Apply Data Mapping](/docs/current/how-tos/accounting-integrations/how-to-apply-data-mapping-for-as-erp-processing)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Detect & Start Export Jobs
Source: https://developers.pleo.io/docs/current/integration-design/exports/integration-design-exports-detect-and-start-export-jobs
This page describes how integrations must:
1. **Detect Export Jobs** that are ready for processing
2. **Start a single Export Job** to begin processing
This is the **first step** in the Export Integration Workflow.
## Implementation
See the corresponding how-to article for API usage and step-by-step instructions:
* [How to Detect and Start Export Jobs for Processing](/docs/current/how-tos/accounting-integrations/how-to-detect-and-start-export-jobs-for-as-erp-processing)
## Conceptual Model
Export processing begins in two distinct phases:
| Phase | Responsibility |
| --------- | --------------------------------------------- |
| Detection | Identify Export Jobs available for processing |
| Starting | Take ownership of a single Export Job |
These phases must remain **logically separate**:
* Detection **must not modify state**
* Starting **is the first state-changing action**
## Detect Export Jobs
### Purpose
Detection identifies Export Jobs that are ready to be processed.
It must:
* Detect newly created Export Jobs
* Identify jobs eligible for processing
* Avoid modifying job state
### Detection Mechanisms
Integrations may use one of the following:
#### Webhooks (preferred)
* Subscribe to `export-job.created`
* Trigger detection when event is received
#### Polling (fallback)
* Periodically call `GET /v3/export-jobs`
* Use controlled intervals (e.g. every few minutes)
#### Ad Hoc Trigger (optional)
* Exposes a user-initiated action (e.g. a button in the integration UI) that immediately runs the same polling flow as schedule polling
* Used **in combination with** scheduled polling, not as a standalone mechanism
* Useful when a user wants to check for pending jobs without waiting for the next scheduled interval
### Eligible Job States
The statuses to include depend on context:
| Context | Statuses | Reason |
| ----------------------- | --------------------------- | ------------------------------------------------- |
| Normal operation | `pending` | Job has not yet been started by any integration |
| Recovery / reconnection | `pending` and `in_progress` | Integration may have an interrupted job to resume |
In normal operation, only `pending` jobs should be fetched. An `in_progress` job found during recovery indicates a previously interrupted workflow: the integration should resume it without sending a `started` event again.
### Sequential Processing Requirement
When multiple jobs exist:
* Always select the **oldest job**
* Process jobs **one at a time**
* Do **not** process jobs in parallel
This prevents:
* race conditions
* duplicate exports
* inconsistent accounting state
Export Jobs only exist after expenses have been [queued](/docs/current/how-tos/accounting-integrations/how-to-queue-export-items-in-ui) in Pleo’s Web App.
## Start Export Job
### Purpose
Starting an Export Job:
* Signals that the integration is starting processing
* Prevents multiple workers processing the same job
* Establishes ownership and traceability
### When to Start
An Export Job must only be started after:
* The job has been discovered
* Pre-processing checks (e.g. validation readiness) have completed
Starting too early can lead to:
* failed exports
* stuck jobs
* inconsistent state
### How Starting Works
Starting only applies to `pending` jobs. If the job is already `in_progress` (recovery scenario), skip the `started` event and resume from pre-export validation.
To start a `pending` job:
1. Select the **oldest eligible job**
2. Send a `started` event via:
```json theme={null}
{
"event": "started",
"jobId": ""
}
```
## Result of Starting
After a successful start:
* Job status transitions to `in_progress`
* The integration becomes responsible for processing
* Export Items can now be fetched
## Concurrency & Conflict Handling
Integrations are designed to run a single export worker, but infrastructure doesn't always guarantee this. Rolling deployments, double-firing scheduled jobs, or a restarting worker can briefly produce two instances that both attempt to start the same job. This is an edge case, not an intended design pattern.
The `started` event acts as an atomic lock: only one instance can successfully start a given job. If starting fails due to a status conflict:
* Treat this as expected behaviour
* Do not retry aggressively
* Restart detection and select the next eligible job
## Key Rules
* Detection must be read-only
* Only one job may be processed at a time. Integrations should run a single export worker; the `started` event enforces this at the API level as a safety net for edge cases such as rolling deployments or double-firing schedulers
* Always process the oldest eligible job
* In normal operation, only `pending` jobs are eligible for detection
* Starting is the first state-changing action and only applies to `pending` jobs
* Starting must be done using the `started` event
* In recovery, an `in_progress` job must be resumed without re-sending the `started` event
## Processing Order
## Upstream Dependencies
* Export Items queued in Pleo Web App
* Integration authentication configured
* Webhooks, scheduled polling, or ad hoc trigger implemented
## Downstream Dependencies
* Pre-export validation
* Export Item retrieval
* AS/ERP processing workflow
***
## What Comes Next?
* [Perform Pre-Export Validation](/docs/current/integration-design/exports/integration-design-exports-pre-export-validation)
***
## Related Reading
* [How to Detect and Start Export Jobs for Processing](/docs/current/how-tos/accounting-integrations/how-to-detect-and-start-export-jobs-for-as-erp-processing)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Retrieve Export Job Items
Source: https://developers.pleo.io/docs/current/integration-design/exports/integration-design-exports-fetch-export-items-control-layer
This page describes how integrations retrieve **Export Job Items** after an Export Job has been started.
Export Job Items represent the **control layer** of the export workflow. They define **which items must be processed** and track **processing state**, but do not contain full accounting data.
## Implementation
See the corresponding how-to article for API usage and step-by-step instructions:
* [How to Retrieve Export Job Items for Processing](/docs/current/how-tos/accounting-integrations/how-to-retrieve-export-job-items-for-as-erp-processing)
## Purpose
* Retrieve all Export Job Items associated with an in-progress Export Job
* Determine the scope of processing
* Track per-item processing state
* Enable resumable and idempotent workflows
## Control Layer vs Data Layer
Export processing is split into two distinct responsibilities:
| Layer | Responsibility |
| ------------------------- | -------------------------------- |
| Control Layer (this step) | Identify items and track status |
| Data Layer | Retrieve full accounting payload |
This step operates **only on control data**.
Export Job Items do not contain full accounting data. Use the Export Items endpoint to retrieve processing payloads.
## Retrieving Export Job Items
Use the **Get Export Job Items** endpoint with the `jobId` of the in-progress Export Job.
```http theme={null}
GET /v3/export-jobs/{jobId}/items
```
This endpoint returns control data for each item, including:
* accountingEntryId (unique identifier)
* status (e.g. pending, in\_progress, successful, failed)
* export tracking fields (externalId, failureReason, etc.)
## Processing Scope
Export Job Items define which entries require processing.
At the start of the workflow:
* All items are typically in `pending` state
* The Export Job is `in_progress`
The integration should determine which items require work.
### Recommended Filtering
Only process items with:
* status = `pending`
* status = `in_progress`
```pseudo theme={null}
itemsToProcess = filter items where status == "pending" or status == "in_progress"
```
## Pagination
Export Job Items may be returned across multiple pages.
Integrations must retrieve all pages before beginning processing.
### Example Pagination Response
```
{
"pagination": {
"hasNextPage": true,
"endCursor": ""
}
}
```
### Example Pseudo
```pseudo theme={null}
items = []
do:
response = fetchItems(cursor)
items.append(response.data)
cursor = response.pagination.endCursor
while response.pagination.hasNextPage
```
Processing a partial dataset may result in incomplete exports and inconsistent state.
## Resumability & Recovery
Export workflows must be resilient to interruptions (e.g. crashes, network failures).
If the integration restarts:
* The Export Job may already be `in_progress`
* Some Export Items may already be processed
### Recovery Strategy
1. Re-fetch all Export Job Items
2. Filter for items still requiring processing
3. Resume from the last known state
```pseudo theme={null}
itemsToProcess = filter items where status == "pending"
```
This ensures the workflow is:
* resumable
* idempotent
* consistent
## Deterministic Processing Order
Export Items should be processed in a stable and predictable order.
Recommended strategies:
* chronological ordering (e.g. oldest transaction date)
* consistent ordering across retries
Deterministic ordering improves:
* reconciliation
* debugging
* auditability
## Idempotency Considerations
The integration must ensure that processing is safe to retry.
Recommended practices:
* Treat `accountingEntryId` as the unique identifier
* Avoid reprocessing items that are already completed
* Maintain local tracking if necessary
## Expected Outcome
After completing this step:
* All Export Job Items have been retrieved
* Processing scope is clearly defined
* Pagination has been resolved
* The workflow is resumable and deterministic
* No accounting data has been processed yet
## Upstream Dependencies
* Export Job has been started (`status = in_progress`)
* Integration authentication is configured
## Downstream Dependencies
* Fetch Export Item Data (data layer)
* AS/ERP processing workflow
* Export Item & Job status updates
***
## What Comes Next?
* [Fetch Export Item Data](/docs/current/integration-design/exports/integration-design-exports-fetch-export-items-data-layer)
***
## Related Reading
* [How to Retrieve Export Job Items for Processing](/docs/current/how-tos/accounting-integrations/how-to-retrieve-export-job-items-for-as-erp-processing)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Fetch Export Item Data
Source: https://developers.pleo.io/docs/current/integration-design/exports/integration-design-exports-fetch-export-items-data-layer
This page describes how integrations retrieve **full Export Item data** after determining which items require processing.
Export Item data represents the **data layer** of the export workflow. It contains the complete accounting payload required to create entries in the target Accounting System.
## Implementation
See the corresponding how-to article for API usage and step-by-step instructions:
* [How to Fetch Export Item Data for Processing](/docs/current/how-tos/accounting-integrations/how-to-fetch-export-item-data-for-as-erp-processing)
## Purpose
* Retrieve full accounting data for Export Items
* Provide all fields required for processing and mapping
* Ensure consistent and complete payloads for downstream workflows
## Control Layer vs Data Layer
Export processing is split into two distinct responsibilities:
| Layer | Responsibility |
| ---------------------- | -------------------------------- |
| Control Layer | Identify items and track status |
| Data Layer (this step) | Retrieve full accounting payload |
This step operates **only on data retrieval**.
## Fetching Export Item Data
Use the **Export Items** endpoint with the `jobId` of the in-progress Export Job.
```http theme={null}
GET /v3/export-items?job_id={jobId}
```
## Data Returned
Each Export Item includes all information required for processing, including:
### Core Transaction Data
* transaction type and subtype
* transaction date
* notes and metadata
### Amounts
* supplier currency
* wallet currency
* net and gross values
### Accounting Entry Lines
* GL account information
* tax codes and rates
* line-level amounts
### Supplier / Vendor Data
* supplier details (name, country, category)
* vendor details (IDs, external references)
### Organisational Context
* user
* team
* cost allocation metadata
### Attachments
* file URLs
* file types and sizes
### Bookkeeping Metadata
* bookkeeping method (e.g. journal, payable)
* additional accounting attributes
## Processing Model
Export Item data is returned as a collection.
Each item must be processed **individually**.
```pseudo theme={null}
items = fetchExportItems(jobId)
for item in items:
process(item)
```
The integration is responsible for applying accounting logic to each item.
## Pagination
Export Items may be returned across multiple pages.
Integrations must retrieve **all pages** before beginning processing.
### Example Pagination Response
```json theme={null}
{
"pagination": {
"hasNextPage": true,
"endCursor": ""
}
}
```
### Example Pseudo
```pseudo theme={null}
items = []
do:
response = fetchItems(cursor)
items.append(response.data)
cursor = response.pagination.endCursor
while response.pagination.hasNextPage
```
Processing incomplete datasets may result in missing accounting entries and inconsistent exports.
### Before Processing: Validate Each Item
Before beginning downstream processing, run two quick checks on each item and immediately mark any that fail. This avoids attempting to record items in your AS that are guaranteed to fail.
For each item check:
1. **Required configurations are present**: the journal is configured and required accounts are mapped for this item
2. **Expense GL Account (Category) is present**: `accountingEntryLines[].account` is populated.
If either check fails, mark the item as `failed` with `failureReasonType: "missing_configuration"` and skip it. Items that pass both checks are ready for processing.
See [How to Update Export Items](/docs/current/how-tos/accounting-integrations/how-to-update-export-items-for-as-erp-processing) for how to report failed items back to Pleo.
## Consistency Guarantees
Export Item data is tied to a specific Export Job.
* Data reflects the state at the time the job was created
* Items within a job should be treated as a consistent batch
* Re-fetching data during recovery should return the same logical dataset
## Relationship to Control Layer
This step must follow the control layer:
1. Retrieve Export Job Items (control layer)
2. Identify items to process (e.g. `pending`)
3. Fetch full Export Item data (this step)
The integration may choose to:
* fetch all data upfront, or
* fetch and process incrementally
## Resumability & Recovery
If the integration restarts:
* Re-fetch Export Item data using the same `jobId`
* Use control layer state to determine which items still require processing
This ensures:
* idempotent processing
* safe retries
* consistent outputs
## Idempotency Considerations
To ensure safe retries:
* Use `accountingEntryId` as the unique identifier
* Avoid duplicate entry creation in the Accounting System
* Coordinate with control layer status tracking
## Expected Outcome
After completing this step:
* Full accounting payloads for all Export Items have been retrieved
* Pagination has been resolved
* Data is ready for processing in the Accounting System
* The workflow remains resumable and consistent
## Upstream Dependencies
* Export Job Items retrieved (control layer)
* Items to process identified (e.g. `pending`)
* Export Job is `in_progress`
## Downstream Dependencies
* AS/ERP processing workflow
* Export Item status updates
* Export Job completion
***
## What Comes Next?
* [Determine Bookkeeping Method](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-resolution)
***
## Related Reading
* [How to Fetch Export Item Data for Processing](/docs/current/how-tos/accounting-integrations/how-to-fetch-export-item-data-for-as-erp-processing)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
## FAQs
You're getting a `MISSING_CONTRA_ACCOUNTS` error, because the Export API v3 requires a contra account to be configured for each entity's default currency. If any entity's default currency does not have a contra account mapped, calls to `/v3/export-items` will fail with this error.
See [How to Resolve MISSING\_CONTRA\_ACCOUNTS](/docs/current/how-tos/accounting-integrations/how-to-resolve-missing-contra-accounts) for steps to fix this.
You're getting a `null` value for the `bookkeeping` field, because it's only populated when the **Vendor Tagging** feature is enabled and a bookkeeping method has been assigned to the expense before it was queued. Without Vendor Tagging enabled, `bookkeeping` will always be `null`. This is expected behaviour, not an error.
When `bookkeeping` is `null`, use the `supplier` field on the export item for bookkeeping purposes instead.
See [How to Enable Vendor-Based Bookkeeping](/docs/current/how-tos/accounting-integrations/how-to-enable-vendor-based-bookkeeping) for steps to enable it.
***
# Exports Overview
Source: https://developers.pleo.io/docs/current/integration-design/exports/integration-design-exports-overview
This section describes **how to implement exports** from Pleo into an Accounting System.
After reviewing [Platform & Capabilities](/docs/current/platform/exports/lifecycle), integrators should now understand:
* What exports represent in Pleo
* Supported bookkeeping methods
* Accounting and data mapping concepts
* Platform export capabilities
This section moves from **concepts** to **implementation design**.
## Design Guarantees
Pleo provides the following guarantees for the exports workflow. Integrations must implement processing logic to fully realise these guarantees.
* **Sequential Consistency**: Export Jobs are queued in order by Pleo, and integrations should process them one at a time to prevent race conditions or accounting conflicts.
* **Deterministic Export Outcomes**: Each Export Item ends in a **successful** or **failed** state as indicated by the integration, ensuring predictable integration behaviour.
* **Failure Isolation**: Failures in individual Export Items do not block other items in the same Export Job.
* **Transparent Reporting**: All outcomes, including errors and processing status from the integration, are surfaced to Pleo Web App users (bookkeepers) where applicable.
* **Platform Alignment**: The workflow aligns with Pleo’s bookkeeping support, including journal entries, accounts payable, and double-entry principles.
## Purpose of the Exports Integration
The export integration transfers validated (approved) expenses from Pleo into an Accounting System in a **predictable, auditable, and reliable manner**.
Exports are processed using a controlled workflow designed to:
* prevent race conditions
* ensure accounting consistency
* provide clear failure reporting
* support retry-safe processing
## Export Processing Model
Exports are processed as **stateful jobs**.
### Export Job
An Export Job represents a batch of expenses selected for export.
It defines:
* processing scope
* execution order
* overall export status
### Export Item
An Export Item represents a single expense within an Export Job.
Each item is processed independently and receives its own success or failure result.
## Export Lifecycle
Export processing follows a deterministic pipeline:
* Each step in the diagram above is clickable and links to its corresponding Integration Design section
* Steps 1–4 and 6–7 interact with Pleo’s Export API (see [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide))
* Step 5 processes export items within the AS/ERP (see [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide))
## Responsibility Model
Export processing is a shared responsibility between Pleo and the integration.
| Pleo Responsibilities | Integration Responsibilities |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------- |
| Prepare export data | Validate Accounting System availability |
| Queue Export Jobs | Retrieve Export Jobs and Items |
| Provide expense data with attachments | Process & record entries in the Accounting System Handle attachments and posting logic |
| Surface integration results to Pleo Web App user | Report success and failure outcomes to Pleo and AS/ERP |
## Processing Principles
All export integrations must follow these principles:
### Sequential Processing
Export Jobs must be processed **one at a time** to avoid accounting inconsistencies.
### Deterministic Outcomes
Each Export Item must end with a final status:
* `successful`
* `failed`
### Failure Isolation
A failed Export Item must **not** stop processing of other items in the same Export Job.
### Idempotent Design
Integrations should safely retry operations without creating **duplicate accounting entries**.
## Expected Outcome
After implementing this workflow, the integration will:
* reliably export expenses into the Accounting System
* provide clear visibility into export outcomes
* support auditing and reconciliation workflows
* align with Pleo’s export processing guarantees
***
## What Comes Next?
* [Detect & Start Export Jobs](/docs/current/integration-design/exports/integration-design-exports-detect-and-start-export-jobs)
***
## Related Reading
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Posting Behaviour
Source: https://developers.pleo.io/docs/current/integration-design/exports/integration-design-exports-posting-behaviour
This page describes how integrations must apply **draft** or **finalised** posting when creating accounting entries in the target Accounting System.
Posting status must be applied **after**:
* Export Items are mapped
* attachments are handled
* accounting periods are assigned
And **before** entries are created in the Accounting System.
## Implementation
See the corresponding how-to article for API usage and step-by-step instructions:
* [How to Apply Posting Behaviour](/docs/current/how-tos/accounting-integrations/how-to-apply-posting-behaviour-for-as-erp-processing)
## Implementation rules
### 1. Read posting configuration (integration configuration)
For custom integrations using Export API v3, the Export API does **not** provide a “posting as draft vs finalised” configuration value.
If your integration needs to support configurable posting status, you must implement this as an **integration configuration** (i.e., a setting in your integration that the user controls).
The configuration determines whether entries are created as:
* **draft**, or
* **finalised** (posted)
### 2. Apply posting status when creating entries
When creating accounting entries, the integration must apply the configured posting behaviour and map it to the closest equivalent state supported by the Accounting System.
| Integration configuration | Required behaviour |
| ------------------------------- | --------------------------- |
| Export in draft status enabled | Create entries as draft |
| Export in draft status disabled | Create entries as finalised |
### 3. Accounting System differences
Accounting Systems may represent posting status differently. Common variants include:
* draft vs posted
* approved vs unapproved
* saved vs committed
The integration must translate the integration’s posting configuration into the closest equivalent posting state supported by the Accounting System.
### 4. Default behaviour
If posting configuration cannot be determined:
* integrations must default to **draft posting**
Defaulting to draft posting allows users to review exported entries before financial impact occurs.
***
## Processing Order
Posting behaviour occurs late in the export workflow:
***
## Upstream Dependencies
* Export Job has been started (status = in\_progress)
* Fetch Export Item Data (data layer)
* Bookkeeping method resolved
* **Accounts Mapping** – determines which GL accounts are debited and credited
* **Data Mapping** – ensures amounts, dates, dimensions, identifiers, and VAT/tax information are recorded correctly
* **Attachment Handling** – links receipts and supporting documentation
* **Accounting Periods** – assigns entries to the correct accounting period
## Downstream Dependencies
* **Export Item status update** – updates outcome of Export Item (`successful` or `failed`)
* **Export Job status update** – updates outcome of Export Job (`completed`, `completed_with_errors` or `failed`)
***
## What Comes Next?
* [Update Export Items](/docs/current/integration-design/exports/integration-design-exports-update-export-items)
***
## Related Reading
* [How to Apply Posting Behaviour](/docs/current/how-tos/accounting-integrations/how-to-apply-posting-behaviour-for-as-erp-processing)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Perform Pre-Export Validation
Source: https://developers.pleo.io/docs/current/integration-design/exports/integration-design-exports-pre-export-validation
Before processing Export Job Items into your Accounting System (AS)/ERP, the integration must validate that the AS/ERP is available and ready to accept entries.
This prevents Export Job Items from being started when processing cannot be completed.
## Implementation
See the corresponding how-to article for API usage and step-by-step instructions:
* [How to Perform Pre-Export Validation](/docs/current/how-tos/accounting-integrations/how-to-perform-pre-export-validation-for-as-erp-processing)
## Purpose
* Confirm Accounting System availability.
* Prevent jobs from becoming stuck in progress.
* Ensure exports start only when processing can succeed.
## Validation Checks
### Accounting System Availability
Confirm that:
* The Accounting System is reachable.
* Authentication is valid.
* The system is not locked or read-only.
* New accounting entries can be created.
## Handling Validation Failures
If validation fails:
1. Mark the Export Job as **failed**.
2. Post a Failed Export Job Event:
* `event` = failed
* `failureReason` = Failed to reach the Accounting System.
* `failureReasonType` = accounting\_system\_unreachable
3. Stop further processing.
## Expected Outcome
* Export Job proceeds only if validation succeeds.
* Failures are reported early with actionable reasons.
* No Export Items are processed yet.
***
## What Comes Next?
* [Retrieve Export Job Items](/docs/current/integration-design/exports/integration-design-exports-fetch-export-items-control-layer)
***
## Related Reading
* [How to Perform Pre-Export Validation](/docs/current/how-tos/accounting-integrations/how-to-perform-pre-export-validation-for-as-erp-processing)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Update & Complete Export Job
Source: https://developers.pleo.io/docs/current/integration-design/exports/integration-design-exports-update-and-complete-export-job
The Export Job must be completed **only after all Export Items have been updated**.
This step communicates the **overall result of the export batch** to Pleo.
## Implementation
See the corresponding how-to article for API usage and step-by-step instructions:
* [How to Update & Complete the Export Job](/docs/current/how-tos/accounting-integrations/how-to-update-and-complete-export-job-for-as-erp-processing)
## Purpose
* Finalise Export Job processing.
* Reflect the aggregated outcome of all Export Items.
* Enable auditing, visibility, and downstream workflows.
## Export Job Status Events
Post an Export Job Event using one of the following outcomes:
| Condition | Event | Additional Fields |
| --------------------------- | ----------------------- | ------------------------------------ |
| All Export Items successful | `completed` | — |
| Some items failed | `completed_with_errors` | — |
| All items failed | `failed` | `failureReason`, `failureReasonType` |
## failureReasonType Guidance
* Default value: `accounting_system_validation_failure`
* Use specialised values when defined in Integration Specifications.
## Implementation Guidance
* Send the Export Job Event only once processing is fully complete.
* Ensure all Export Item statuses are final before completing the job.
* Do not complete jobs prematurely.
Completing the Export Job signals to Pleo that:
* Processing has finished.
* Results can be surfaced to users.
* Follow-up workflows may begin.
## Expected Outcome
At the end of this step:
* The Export Job reflects the true batch outcome.
* Users can review results in Pleo.
* The export lifecycle is complete.
## Upstream Dependencies
* Export Job has been started (status = in\_progress)
* Fetch Export Item Data (data layer)
* Bookkeeping method resolved
* **Accounts Mapping** – determines which GL accounts are debited and credited
* **Data Mapping** – ensures amounts, dates, dimensions, identifiers, and VAT/tax information are recorded correctly
* **Attachment Handling** – links receipts and supporting documentation
* **Accounting Periods** – assigns entries to the correct accounting period
* **Posting Behaviour** – determines whether entries are created as drafts or finalised
* **Export Item status update** – updates outcome of Export Item (`successful` or `failed`)
***
## What Comes Next?
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
***
## Related Reading
* [How to Update & Complete the Export Job](/docs/current/how-tos/accounting-integrations/how-to-update-and-complete-export-job-for-as-erp-processing)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Update Export Items
Source: https://developers.pleo.io/docs/current/integration-design/exports/integration-design-exports-update-export-items
Export Items must be updated **after entries have been recorded in the Accounting System**, including attachment handling, accounting period assignment, and posting behaviour.
This step communicates the **final outcome of each individual Export Item** back to Pleo.
## Implementation
See the corresponding how-to article for API usage and step-by-step instructions:
* [How to Update Export Items](/docs/current/how-tos/accounting-integrations/how-to-update-export-items-for-as-erp-processing)
## Purpose
* Report success or failure for every Export Item.
* Provide traceability between Pleo and the Accounting System.
* Surface actionable error messages to users.
* Enable accurate Export Job completion.
## Definitions
* **External URL**: Direct link to the created entry in the Accounting System.
* **External ID**: Unique identifier assigned by the Accounting System.
* **Exported At**: Timestamp when the entry was successfully created.
## Export Item Status Updates
Export Items must be updated in **batches of up to 100**.
Each Export Item must be assigned one final status.
### Successful Export Item
Set:
* `status = successful`
Provide when available:
* `externalUrl`
* `externalId`
* `exportedAt`
### Failed Export Item
Set:
* `status = failed`
Provide:
* `failureReasonType`
* `failureReason`
Failure messages must be clear, actionable, and based on the Accounting System response where possible.
## Validation Failure Errors
Use the following mappings when validation prevents processing:
| Validation Error | failureReasonType | failureReason |
| -------------------------- | ----------------------- | ------------------------------------------------------------ |
| Missing Journal | `missing_configuration` | “Journal X missing: provide journal X in the configurations” |
| Missing Account | `missing_configuration` | “Account X missing: provide account X in the configurations” |
| Missing Expense GL Account | `missing_configuration` | “Account X missing: provide account X in the configurations” |
## Common Export Errors
| Error | failureReasonType | failureReason |
| -------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Tax Code missing in AS | `invalid_configuration` | “Tax Code X does not exist in the Accounting System” |
| Expense GL Account missing | `invalid_configuration` | “Account No. X does not exist in the Accounting System. Check Category.” |
| Tag missing | `invalid_configuration` | “Tag with code X does not exist on the Accounting System. Check the Tag.” |
| Vendor not found in AS | `vendor_unknown` | “Selected vendor cannot be processed because it does not exist or is currently blocked. Please verify the vendor's status in the accounting system and try again.” |
| Attachment too large | `receipt_file_size_limit_exceeded` | “Attachment file size exceeds accounting system limits.” |
| Receipt download failed | `receipt_download_failure` | “Failed to download the attachment file from Pleo. Re-export the expense.” |
| Receipt upload failed | `receipt_upload_failure` | Pass Accounting System error message |
| API rate limit exceeded | `accounting_system_rate_limit` | Pass Accounting System error message |
| Authorisation failure | `accounting_system_authentication_failure` | Pass Accounting System error message |
## Other Export Errors
For all remaining failures:
* `failureReasonType = accounting_system_validation_failure`
* `failureReason =` error message returned by the Accounting System
## Expected Outcome
At the end of this step:
* Every Export Item has a final status.
* Successful items include Accounting System references.
* Failed items include actionable error information.
## Upstream Dependencies
* Export Job has been started (status = in\_progress)
* Fetch Export Item Data (data layer)
* Bookkeeping method resolved
* **Accounts Mapping** – determines which GL accounts are debited and credited
* **Data Mapping** – ensures amounts, dates, dimensions, identifiers, and VAT/tax information are recorded correctly
* **Attachment Handling** – links receipts and supporting documentation
* **Accounting Periods** – assigns entries to the correct accounting period
* **Posting Behaviour** – determines whether entries are created as drafts or finalised
## Downstream Dependencies
* **Export Job status update** – updates outcome of Export Job (`completed`, `completed_with_errors` or `failed`)
***
## What Comes Next?
* [Update & Complete Export Job](/docs/current/integration-design/exports/integration-design-exports-update-and-complete-export-job)
***
## Related Reading
* [How to Update Export Items](/docs/current/how-tos/accounting-integrations/how-to-update-export-items-for-as-erp-processing)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Integration Principles
Source: https://developers.pleo.io/docs/current/integration-design/overview/integration-design-principles-overview
This section outlines key principles for designing and developing Pleo accounting
integrations. These principles ensure integrations are efficient, scalable,
maintainable, and meet user expectations and business needs.
### Integration Scope
* All integrations must implement at least [**Level 2 functionality**](/docs/current/getting-started/accounting-integrations-overview#integration-levels), although
**Level 4** is recommended for full automation and user experience.
### Minimum States
* Retain state only for:
* Authentication (e.g., API keys, OAuth 2.0 tokens)
* Configuration parameters (user-provided settings required to run the integration)
* Avoid storing any data already stored in Pleo or the target system, unless
unavoidable for implementing integration functionality.
* The target ERP/Accounting System is the primary source of truth for mapping
entities and codes.
* Temporary caching is acceptable to improve the user experience during setup
(e.g., populating dropdown selections).
### Self-Service Setup
* Integrations should allow users to configure and activate them independently
through a self-service process.
### Resources Provided by Pleo
* All Pleo resources are available via this Developer Portal for reference and
implementation guidance.
### API Rate Limit
* Integrations should be designed to avoid exceeding the API rate limits of
Pleo and target systems, taking into account batch processing, retries, and
any automated synchronisation. Each [API Reference](/reference/api-introduction) section has their own API Rate limits outlined.
# Classifying Expenses: Tags, Categories, and Tax Codes
Source: https://developers.pleo.io/docs/current/platform/accounting-integrations/expense-classification-overview
Tags, Categories, and Tax Codes are the three pieces of accounting metadata a spender interacts with when they submit an expense in Pleo. Each piece is kept up to date by a different import sync, but they all come together on the same expense:
* **Tags Sync** brings in Accounting Dimensions and Values as Tag Groups and Tags, which spenders can assign to an expense
* **Chart of Accounts Sync** brings in accounts, which bookkeepers map to Categories that spenders select on an expense
* **Tax Sync** brings in Tax Codes, which bookkeepers set as the default on a Category so the correct tax code is auto-selected when a spender picks that Category
Each sync adds one puzzle piece. None of them alone is the full picture, but together they determine how an expense is classified before it is exported back to the Accounting System.
## How the Pieces Fit Together
1. **Tags Sync** and **Chart of Accounts Sync** and **Tax Sync** each run independently, bringing their respective data into Pleo from the Accounting System
2. A bookkeeper maps synced accounts to Categories, and sets a default Tax Code on each Category, in the Pleo Web App
3. When a spender submits an expense, they select a Category and can assign Tags; the Category's default Tax Code is auto-selected
4. The bookkeeper confirms or overrides these before the expense is exported back to the Accounting System with its Category, Tax Code, and Tags attached
## The Three Import Syncs
Syncs Accounting Dimensions and Values into Pleo as Tag Groups and Tags, which spenders assign to expenses.
Syncs the Chart of Accounts into Pleo, which bookkeepers map to Categories that spenders select.
Syncs Tax Codes into Pleo, which bookkeepers set as the default on a Category.
## Configuring Tags, Categories and Tax Codes
### Syncing These with Your Custom Integration
If you're building a custom integration, the how-to articles cover the implementation-level detail: curl requests, pseudo code, and screenshots showing how the resulting changes appear in the Pleo Web App.
Step-by-step how-to articles for syncing Tags, Chart of Accounts, and Tax Codes.
### Configuring These Without a Custom Integration
Tags, Categories, and Tax Codes can all be set up manually in the Pleo Web App if you don't need a custom sync:
* [How to Set Up Your Tax Codes](https://help.pleo.io/en/support/solutions/articles/103000254846-how-to-set-up-your-tax-codes)
* [How to Set Up Your Categories](https://help.pleo.io/en/support/solutions/articles/103000254857-how-to-set-up-your-categories) (includes a walkthrough video)
* [How to Set Up Your Tags](https://help.pleo.io/en/support/solutions/articles/103000254859-how-to-set-up-your-tags) (includes a walkthrough video)
## Exporting the Expense to Your AS
Once an expense carries a Category, Tax Code, and Tags, it is ready to be exported to the Accounting System.
Understand the end-to-end lifecycle of exporting an expense: job creation, detection, validation, per-item processing, and status updates.
### Exporting Without a Custom Integration
If you don't need a custom export integration, expenses can be exported manually from the Pleo Web App:
* [How to Export Expenses](https://help.pleo.io/en/support/solutions/articles/103000303993-how-to-export-expenses)
***
## Related Reading
* [Tags Sync Overview](/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview)
* [Chart of Accounts Sync Overview](/docs/current/platform/accounting-integrations/imports/accounts/accounts-sync-overview)
* [Tax Sync Overview](/docs/current/platform/accounting-integrations/imports/tax-codes/tax-sync-overview)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [Accounts Payable & Invoices](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-accounts-payable)
* [Vendors API Overview](/reference/vendor/vendors-api-overview)
* [Paying a Supplier Invoice Using Pleo Web App](https://help.pleo.io/en/support/solutions/articles/103000285539-paying-a-supplier-invoice)
***
# Chart of Accounts Sync Overview
Source: https://developers.pleo.io/docs/current/platform/accounting-integrations/imports/accounts/accounts-sync-overview
**Chart of Accounts Sync** is how Pleo keeps its Chart of Accounts up to date by synchronising account records from an external Accounting System into Pleo.
It ensures bookkeepers always have an accurate, current list of accounts to reference when mapping expense categories to accounts in Pleo, without having to manually maintain those accounts.
## Purpose of Chart of Accounts Sync
Chart of Accounts Sync exists to:
* Remove the need to manually create or maintain accounts in Pleo
* Reflect the current state of the Chart of Accounts from the Accounting System
* Enable accurate bookkeeping by ensuring expense categories map to the correct accounts
* Keep account data synchronised between systems
## Core Concept
Chart of Accounts Sync is based on a direct mapping between systems:
| Accounting System | Pleo |
| -------------------------- | --------------------------- |
| Chart of Accounts entry | Account |
| Account identifier | Account `externalId` |
| Account number | Account `code` |
| Account name | Account `name` |
| Default tax code reference | Account `taxCodeExternalId` |
For detailed field-level mapping, see [Chart of Accounts Sync Data Mapping](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-data-mapping).
### How Accounts Are Used in Pleo
Accounts synced into Pleo are not assigned directly to expenses. Instead, bookkeepers map each expense category to an account in the Pleo Web App. When a spender assigns a category to an expense, the account mapped to that category is what gets used for bookkeeping and exported to the Accounting System.
The full chain is:
1. **Accounting System Chart of Accounts:** the source of account data
2. **Pleo Chart of Accounts:** accounts are synced in and kept up to date via Chart of Accounts Sync
3. **Pleo Categories:** bookkeepers map each category to an account in the Pleo Web App
4. **Expenses:** when a spender selects a category on an expense, the mapped account is used for bookkeeping
This is why keeping Pleo's Chart of Accounts accurate matters: if the account data is stale or incorrect, categories may reference the wrong accounts when expenses are exported.
### What Happens in Pleo Once Synchronised
* Each active account from the Accounting System is reflected as an account in Pleo
* Accounts are available for bookkeepers to assign to expense categories
* If an account's name or code changes in the Accounting System, it is updated in Pleo on the next sync
* Accounts no longer active in the Accounting System are archived in Pleo
This ensures expense categories always reference the correct accounts without requiring manual input.
If the Accounting System has a default tax code per account, that identifier can be stored in Pleo via `taxCodeExternalId`. This is a reference only. Pleo does not automatically link it to a Tax Code in Pleo, apply it to expenses, or update the default tax code on any category mapped to the account.
## System Guarantees
These are invariant rules that always hold true regardless of implementation or sync frequency.
| Guarantee | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------- |
| Source of Truth | The Accounting System is the authoritative source of all account data |
| Structural Mapping | Accounts map directly from the Accounting System into Pleo |
| Identity Model | Matching is performed using `externalId`, not account name or code |
| Non-Destructive Behaviour | Accounts are never deleted; they are only archived when no longer active in the Accounting System |
| Idempotency | Re-running a sync produces the same final state without duplication |
## Responsibility Model
### Integrator Responsibilities
The integration is responsible for all synchronisation logic and API interaction.
| Area | Responsibility |
| -------------- | ----------------------------------------------------------------------- |
| Connection | Connect to the Accounting System |
| Data Retrieval | Retrieve active accounts from the Chart of Accounts |
| Sync Execution | Execute reconciliation operations in Pleo via the Chart of Accounts API |
| Scheduling | Define and execute sync schedule (including optional manual triggers) |
### Pleo Responsibilities
Pleo provides storage, user experience, and downstream usage of accounts.
| Area | Responsibility |
| ---------------- | ---------------------------------------------------------- |
| Data Storage | Store accounts |
| User Interface | Display accounts for category assignment |
| Category Mapping | Allow bookkeepers to assign accounts to expense categories |
| Data Retention | Retain archived accounts for historical consistency |
## High-Level Process
Chart of Accounts Sync runs as a single reconciliation process:
1. **Fetch active accounts** from the Accounting System
2. **Fetch accounts from Pleo** (active and archived)
3. **Reconcile:** match by `externalId` and apply create, update, unarchive, or archive operations
Each sync run reconciles Pleo against the current state of the Accounting System.
## Operational Model
### Sync Direction
Chart of Accounts Sync is one-way only:
* The **Accounting System** is the **source of truth**
* The integration synchronises data from the Accounting System into Pleo
* Pleo does not modify Accounting System data
### Operational Constraints
| Constraint | Description |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Scheduling Ownership | The integration is responsible for defining when sync runs occur, including optional manual triggers |
| Sync Execution | The integration is responsible for executing sync runs and applying all changes to Pleo via the API |
| Execution Model | Each sync run performs full reconciliation of Pleo against the Accounting System |
| Rate Limits | The integration must respect Accounting System API rate limits. See [Pleo API rate limits](/docs/current/authentication/api-base-urls#rate-limits). |
| Concurrency | Sync runs must not overlap; only one execution may run at a time |
### Sync Lifecycle and Frequency
The integration is responsible for triggering all sync runs. For schedule, frequency, and rate limit details, see [Chart of Accounts Sync Periodicity and Scheduling](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-periodicity).
***
## Related Reading
* [Integration Design: Chart of Accounts Sync Overview](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-overview)
* [Chart of Accounts Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/accounts-sync-workflow-guide)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# Tags Sync Overview
Source: https://developers.pleo.io/docs/current/platform/accounting-integrations/imports/tags/tags-sync-overview
**Tags Sync** is how Pleo keeps its Tags up to date by synchronising Accounting Dimensions from an external Accounting System into Pleo.
It ensures that bookkeepers always work with an accurate and current representation of Accounting Dimensions when categorising expenses in Pleo.
## Purpose of Tags Sync
Tags Sync exists to:
* Remove the need to manually create or maintain Tags in Pleo
* Reflect the current structure of Accounting Dimensions in Pleo
* Enable consistent expense categorisation aligned with the Accounting System
* Keep accounting metadata synchronised between systems
## Core Concept
Tags Sync is based on a direct mapping between systems:
| Accounting System | Pleo |
| -------------------- | --------- |
| Accounting Dimension | Tag Group |
| Dimension Value | Tag |
For detailed field-level mapping, see [Tags Sync Data Mapping](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-data-mapping).
### How Tags Are Used in Pleo
Once synced, Tags are available for spenders to assign to expenses. When an expense is exported, the assigned Tags are included, allowing the Accounting System to categorise and report on expenses using its own Dimension structure.
The full chain is:
1. **Accounting System Dimensions and Values:** the source of tag structure, synced into Pleo via Tags Sync
2. **Expenses:** when a spender submits an expense, they can apply Tags to it
3. **Export:** the expense is exported to the Accounting System with the assigned Tags attached
### What Happens in Pleo Once Synchronised
* Each Accounting Dimension becomes a Tag Group in Pleo
* Each Dimension Value becomes a Tag
* Each Tag belongs to exactly one Tag Group
* Bookkeepers use Tags to categorise expenses
* Tags are used in downstream export processes (outside of Tags Sync)
This ensures expenses reflect the Accounting System structure without manual setup in Pleo.
## System Guarantees
These are invariant rules that always hold true regardless of implementation or sync frequency.
| Guarantee | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Source of Truth | The Accounting System is the authoritative source of all Dimensions and Dimension Values |
| Structural Mapping | Dimensions map to Tag Groups, and Dimension Values map to Tags |
| Identity Model | Matching is performed using codes, not names |
| Case-Insensitive Matching | Code matching ignores case differences |
| Non-Destructive Behaviour | Tags and Tag Groups are never deleted; they are only archived when no longer present in the Accounting System |
| Idempotency | Re-running a sync produces the same final state without duplication |
## Responsibility Model
### Integrator Responsibilities
The integration is responsible for all synchronisation logic and API interaction.
| Area | Responsibility |
| -------------- | --------------------------------------------------------------------- |
| Connection | Connect to the Accounting System |
| Data Retrieval | Retrieve Dimensions and Dimension Values |
| Configuration | Manage which Dimensions are selected for synchronisation |
| Sync Execution | Execute reconciliation operations in Pleo via the Tags API |
| Scheduling | Define and execute sync schedule (including optional manual triggers) |
### Pleo Responsibilities
Pleo provides storage, user experience, and downstream usage of Tags.
| Area | Responsibility |
| ------------------ | -------------------------------------------------------------- |
| Data Storage | Store Tag Groups and Tags |
| User Interface | Display Tags for expense categorisation |
| Expense Assignment | Allow users to assign Tags to expenses |
| Data Retention | Retain archived Tags and Tag Groups for historical consistency |
## High-Level Process
Tags Sync runs in three steps:
1. **Select Dimensions**: configure which Accounting Dimensions to synchronise (first connection only)
2. **Sync Tag Groups with Dimensions**: reconcile Tag Groups in Pleo with the selected Dimensions
3. **Sync Tags with Dimension Values**: reconcile Tags within each Tag Group with the corresponding Dimension Values
Each sync run reconciles Pleo against the current state of the Accounting System.
## Operational Model
### Sync Direction
Tags Sync is one-way only:
* The **Accounting System** is the **source of truth**
* The integration synchronises data from the Accounting System into Pleo
* Pleo does not modify Accounting System data
* Pleo data is only used for identity matching and reconciliation of existing entities, never as a source of truth for Dimensions or Dimension Values
### Operational Constraints
These define how the integration must execute Tags Sync.
| Constraint | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Scheduling Ownership | The integration is responsible for defining when sync runs occur, including optional manual triggers |
| Sync Execution | The integration is responsible for executing sync runs and applying all changes to Pleo via the API |
| Execution Model | Each sync run performs full reconciliation of Pleo against the Accounting System |
| Rate Limits | The integration must respect Accounting System API rate limits and adjust scheduling accordingly. See [Pleo API rate limits](/docs/current/authentication/api-base-urls#rate-limits). |
| Concurrency | Sync runs must not overlap; only one execution may run at a time |
### Sync Lifecycle and Frequency
The integration is responsible for triggering all sync runs. For schedule, frequency, and rate limit details, see [Tags Sync Periodicity and Scheduling](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-periodicity).
## Dimension Selection
The integration controls which Accounting Dimensions participate in Tags Sync. Only selected Dimensions are synchronised into Pleo.
Dimension selection determines:
* which Dimensions are included in reconciliation
* which Tag Groups and Tags are maintained in Pleo
* which accounting structures become available for expense categorisation
Detailed selection behaviour and implementation guidance are covered in [Integration Design](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-dimension-selection).
***
## Related Reading
* [Integration Design: Tags Sync Overview](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-overview)
* [Tags Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tags-sync-workflow-guide)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# Tax Sync Overview
Source: https://developers.pleo.io/docs/current/platform/accounting-integrations/imports/tax-codes/tax-sync-overview
**Tax Sync** is how Pleo keeps its Tax Codes up to date by synchronising tax code records from an external Accounting System into Pleo.
It ensures bookkeepers always have an accurate, current list of tax codes to reference when assigning tax codes to expenses in Pleo, without having to manually maintain those codes.
## Purpose of Tax Sync
Tax Sync exists to:
* Remove the need to manually create or maintain Tax Codes in Pleo
* Reflect the current state of active tax codes from the Accounting System
* Enable accurate bookkeeping by ensuring expenses carry the correct tax codes before export
* Keep tax code data synchronised between systems
## Core Concept
Tax Sync is based on a direct mapping between systems:
| Accounting System | Pleo |
| ------------------- | -------------------------- |
| Tax code entry | Tax Code |
| Tax code identifier | Tax Code `code` |
| Tax code name | Tax Code `name` |
| Tax rate | Tax Code `rate` |
| Reverse tax | Tax Code `type: reverse` |
| All other taxes | Tax Code `type: inclusive` |
For detailed field-level mapping, see [Tax Sync Data Mapping](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-data-mapping).
### How Tax Codes Are Used in Pleo
Tax Sync works alongside Chart of Accounts Sync as part of Level 3. Both must be in place for the full tax assignment flow to work: accounts are synced and mapped to categories, tax codes are synced, and the bookkeeper then sets a default tax code on each category in the Pleo Web App. From that point, when a spender selects a category on an expense, the default tax code is automatically applied.
The full chain is:
1. **Accounting System Tax Codes:** the source of tax code data, synced into Pleo via Tax Sync
2. **Accounting System Chart of Accounts:** accounts are synced into Pleo via Chart of Accounts Sync and mapped to categories by the bookkeeper
3. **Pleo Categories:** the bookkeeper sets a default tax code on each category in the Pleo Web App
4. **Expenses:** when a user selects a category, the default tax code is auto-selected; the bookkeeper confirms or overrides before export
5. **Export:** the expense is exported to the Accounting System with the assigned tax code
Setting a default tax code on a category must be done explicitly by the bookkeeper in the Pleo Web App. It is not set automatically by Tax Sync or Chart of Accounts Sync.
This is why keeping Pleo's Tax Codes accurate matters: if the tax code data is stale or incorrect, expenses may carry the wrong tax codes when exported.
### What Happens in Pleo Once Synchronised
* Each active, relevant tax code from the Accounting System is reflected as a Tax Code in Pleo
* Tax Codes are available for bookkeepers to assign to expenses
* If a tax code's name or rate changes in the Accounting System, it is updated in Pleo on the next sync
* Tax Codes no longer active in the Accounting System are archived in Pleo
This ensures expenses always reference the correct tax codes without requiring manual input.
Only tax codes relevant to expense management are synced. Tax codes not relevant to expense bookkeeping (such as Sales VAT) and blocked or inactive tax codes are filtered out before syncing.
## System Guarantees
These are invariant rules that always hold true regardless of implementation or sync frequency.
| Guarantee | Description |
| ------------------------- | -------------------------------------------------------------------------------------------------- |
| Source of Truth | The Accounting System is the authoritative source of all tax code data |
| Structural Mapping | Tax Codes map directly from the Accounting System into Pleo |
| Identity Model | Matching is performed using `code`, not tax code name |
| Non-Destructive Behaviour | Tax Codes are never deleted; they are only archived when no longer active in the Accounting System |
| Idempotency | Re-running a sync produces the same final state without duplication |
## Responsibility Model
### Integrator Responsibilities
The integration is responsible for all synchronisation logic and API interaction.
| Area | Responsibility |
| -------------- | --------------------------------------------------------------------------------------------------- |
| Connection | Connect to the Accounting System |
| Data Retrieval | Retrieve active, relevant tax codes from the Accounting System |
| Filtering | Exclude tax codes not relevant to expense management (e.g. Sales VAT) and blocked or inactive codes |
| Sync Execution | Execute reconciliation operations in Pleo via the Tax Codes API |
| Scheduling | Define and execute sync schedule (including optional manual triggers) |
### Pleo Responsibilities
Pleo provides storage, user experience, and downstream usage of tax codes.
| Area | Responsibility |
| -------------- | ---------------------------------------------------- |
| Data Storage | Store Tax Codes |
| User Interface | Display Tax Codes for assignment to expenses |
| Data Retention | Retain archived Tax Codes for historical consistency |
## High-Level Process
Tax Sync runs as a single reconciliation process:
1. **Fetch relevant active tax codes** from the Accounting System (filtering out irrelevant or inactive codes)
2. **Fetch Tax Codes from Pleo** (active and archived)
3. **Reconcile:** match by `code` and apply create, update, unarchive, or archive operations
Each sync run reconciles Pleo against the current state of the Accounting System.
## Operational Model
### Sync Direction
Tax Sync is one-way only:
* The **Accounting System** is the **source of truth**
* The integration synchronises data from the Accounting System into Pleo
* Pleo does not modify Accounting System data
### Operational Constraints
| Constraint | Description |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Scheduling Ownership | The integration is responsible for defining when sync runs occur, including optional manual triggers |
| Sync Execution | The integration is responsible for executing sync runs and applying all changes to Pleo via the API |
| Execution Model | Each sync run performs full reconciliation of Pleo against the Accounting System |
| Rate Limits | The integration must respect Accounting System API rate limits. See [Pleo API rate limits](/docs/current/authentication/api-base-urls#rate-limits). |
| Concurrency | Sync runs must not overlap; only one execution may run at a time |
### Sync Lifecycle and Frequency
The integration is responsible for triggering all sync runs. For schedule, frequency, and rate limit details, see [Tax Sync Periodicity and Scheduling](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-periodicity).
***
## Related Reading
* [Integration Design: Tax Sync Overview](/docs/current/integration-design/accounting-integrations/imports/tax-codes/integration-design-tax-sync-overview)
* [Tax Sync Workflow Guide](/docs/current/guides/accounting-integrations/imports/tax-sync-workflow-guide)
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
***
# Employees and Roles
Source: https://developers.pleo.io/docs/current/platform/employees/employees-and-roles
Pleo supports multiple user roles to manage spending, approvals, bookkeeping, and integrations.
Understanding how employees and roles are represented in Pleo is essential when designing integrations, configuring permissions, and automating workflows.
This page explains the **conceptual model** of users and roles in Pleo and highlights which permissions matter for accounting and API integrations.
## Employee Types in Pleo
Pleo distinguishes between two main types of users:
### Internal employees
Internal employees are members of the organisation who:
* Use Pleo cards for business spending
* Submit out-of-pocket expenses for reimbursement
* Participate in approval and review workflows
### External bookkeepers
External bookkeepers are third-party users (for example, accountants) invited to manage bookkeeping tasks without being employees of the organisation.
External bookkeepers:
* Do not have access to Pleo cards
* Have restricted permissions depending on access level
* Are commonly used in agency or outsourced accounting setups
## User Roles Overview
Pleo assigns permissions through roles. Each role defines what actions a user can perform in the platform.
| Role | Purpose | Integration relevance |
| -------------------- | --------------------------------------- | ---------------------------------- |
| **Employee** | Submits expenses and uses cards | Source of expense data |
| **Administrator** | Full platform control | Required to configure integrations |
| **Expense reviewer** | Reviews and approves expenses | Controls accounting readiness |
| **Bookkeeper** | Manages accounting settings and exports | Controls export behaviour |
| **Finance** | Financial oversight and reporting | Audit and analysis |
## Role Permissions and Constraints
### Administrators
Administrators have full access to the Pleo platform, including:
* Managing employees and roles
* Managing wallets and cards
* Configuring ERP and Accounting System integrations
* Exporting accounting data
* Managing subscriptions and invoices
**Admins are required** to:
* Configure API integrations
* Generate API keys or OAuth 2.0 applications
* Grant access to bookkeepers and reviewers
### Expense reviewers
Expense reviewers:
* Review submitted expenses
* Approve or reject expenses based on policy
* Control whether expenses become accounting-ready
Approved expenses are the **only expenses eligible for export** via the API.
### Bookkeepers
Bookkeepers manage accounting-related workflows.\
There are two access levels:
#### Limited access
* View and modify expense details
* View invoices (cannot pay invoices)
* Modify accounting settings
* Export accounting entries
* Reimburse out-of-pocket expenses
* Move funds through the Pleo wallet (optional)
#### Extended access
Includes all limited permissions, plus:
* Add employees and manage roles
* Configure spending limits
* Order cards
* Assign expense reviewer permissions
* Perform wallet top-ups (region-dependent)
## Employees and Integrations
* Employees are scoped **per company**
* Integrations interact with employees only within the company context
* HRIS integrations may be used to automatically provision and deactivate users
* External identifiers can be stored to reconcile users across systems
Correct role assignment is critical to:
* Enable exports
* Ensure approvals are completed
* Maintain auditability
## What comes next?
Follow the relevant guides to manage users in Pleo:
* [Add employees to Pleo](/docs/current/how-tos/user-management/how-to-add-employees-to-pleo)
* [Assign user roles](/docs/current/how-tos/user-management/how-to-assign-user-roles-to-employees)
* [Invite external bookkeepers](/docs/current/how-tos/user-management/how-to-invite-external-bookkeepers)
# Accounting Periods
Source: https://developers.pleo.io/docs/current/platform/exports/accounting-periods
Pleo does not manage or assign accounting periods.
Each Export Item includes a **transaction date**, which the integration must use to determine the appropriate accounting period in the connected Accounting System.
## Core Principle
Pleo provides:
* The **transaction date** of the expense
The integration is responsible for:
* Determining the correct accounting period
* Handling closed periods
* Applying any date adjustments required by the Accounting System
## Period Determination
The integration must map the provided transaction date to an accounting period based on:
* Accounting System period definitions
* Fiscal or custom calendars
* Period open/closed status
Pleo does not interpret or validate accounting periods.
## Closed Period Handling
If an expense falls into a closed period, the integration must decide how to handle it.
Typical approaches include:
* Shifting the date to the next open period
* Posting to a designated adjustment period
* Rejecting the export and surfacing an error
Pleo does not modify or adjust transaction dates.
## Custom Accounting Periods
Custom accounting structures are fully supported through the integration.
The integration may assign expenses to:
* Fiscal periods
* Custom calendars
* Non-monthly accounting structures
based on Accounting System rules.
## Platform Behaviour
* Transaction dates are always included in Export Items
* No accounting period logic is applied by Pleo
* No validation against Accounting System period rules is performed
* Final period assignment occurs in the Accounting System via the integration
***
## What Comes Next?
* [Posting Behaviour](/docs/current/platform/exports/posting-behaviour)
***
## Related Reading
* [Integration Design - Accounting Periods](/docs/current/integration-design/exports/integration-design-exports-accounting-periods)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Accounts Mapping
Source: https://developers.pleo.io/docs/current/platform/exports/accounts-mapping
Accounts mapping determines which General Ledger accounts are affected when exported expenses are recorded in the Accounting System. Correct mapping ensures financial accuracy, reconciliation, and compliance with bookkeeping standards.
After the **bookkeeping method** has been determined (for example, Journal Entry or Accounts Payable), exported expenses must be connected to the correct **General Ledger (GL) accounts**.
Accounts mapping defines **where the financial impact of an expense is recorded** in the Accounting System.
## Why Accounts Mapping Exists
All exported expenses follow **double-entry bookkeeping**, meaning every transaction records:
* one or more **debit postings**
* one or more **credit postings**
The bookkeeping method determines *how* an expense is represented (journal entry vs invoice/payment), while **accounts mapping determines which accounts are debited and credited**.
Conceptually:
## What Is Being Mapped?
Accounts mapping connects Pleo data to accounting structures by assigning GL accounts for different financial roles, including:
* Expense categorisation
* Funding source or settlement account
* Vendor liability tracking
* Temporary or balancing accounts
## Required Accounts
The following accounts are typically required to support exports. Exact naming and structure depend on the Accounting System configuration.
### Pleo Wallet Account (user-configurable)
Tracks funds spent using the Pleo wallet or company cards.
Typically represents:
* a bank account, or
* a clearing account used for card transactions.
### Pleo Out-of-Pocket Account (user-configurable)
Tracks expenses initially paid by employees and later reimbursed.
Represents a temporary liability owed to employees until reimbursement occurs.
### Pleo Contra Account (user-configurable)
An intermediate balancing account used when the final accounting destination is not yet known or when funds move between internal sources.
Common examples include:
* wallet loads or unloads
* external reimbursements
* adjustment transactions
### Category (Expense GL Account)
Defines the expense classification.
Each Export Item includes a category selected in Pleo, which maps to an expense GL account such as:
* Travel
* Meals
* Software subscriptions
* Office supplies
This account is typically **debited** when an expense is recorded.
### Accounts Payable Account
Used when **Accounts Payable bookkeeping** is selected.
Tracks vendor liabilities and may be configured as:
* vendor-specific subledger accounts, or
* a single shared Accounts Payable control account.
## Mapping Depends on Bookkeeping Method
Accounts mapping applies differently depending on how the expense is represented in the Accounting System.
### Journal Entry Bookkeeping
Mapping determines:
* which expense account is debited
* which funding or liability account is credited
The result is a direct posting to the general ledger.
### Accounts Payable Bookkeeping
Mapping determines accounts across multiple accounting objects:
* Invoice entries (expense + Accounts Payable liability)
* Payment entries (settlement of liability)
* Credit notes where applicable
Although more accounting records are created, the same mapped accounts ensure consistent financial reporting.
## Expense Type Mapping
Different expense types may require different account behaviour.
For detailed mapping rules by expense type, see:
[Expense Type Mapping](https://docs.google.com/spreadsheets/d/1WnA4QLYKTfeabsSAaR8Jfy2veJpGCrP-jAAFX9JeB3c/edit?gid=942665485#gid=942665485)
## Relationship to the Export Lifecycle
Accounts mapping occurs during **per-item processing**, after:
* Export validation
* Bookkeeping method resolution
and before:
* accounting period assignment
* posting behaviour application
* export item completion
This ensures every exported record enters the Accounting System with valid debit and credit destinations.
***
## What Comes Next?
* [Data Mapping](/docs/current/platform/exports/data-mapping)
***
## Related Reading
* Integration Design – [Accounts Mapping](/docs/current/integration-design/exports/integration-design-exports-accounts-mapping)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Attachments
Source: https://developers.pleo.io/docs/current/platform/exports/attachments
Pleo supports exporting attachments associated with expenses, such as receipts and supporting documents.
Attachments are provided as part of Export Items and can be retrieved by integrations for storage in an Accounting System.
## Attachment Availability
Attachments are included when:
* an expense has one or more uploaded files
* the files are accessible at export time
* the Export Item passes validation
Each attachment is exposed through the Export Item payload.
## Attachment Data Model
Attachments are available via:
* `files.url` — secure download URL
* `files.type` — file MIME type
* `files.size` — file size in bytes
The integration is responsible for downloading and storing attachments in the destination system.
## Supported File Formats
Pleo supports the following attachment formats:
* `.doc`, `.docx`
* `.pdf`
* `.txt`
* `.jpg`, `.jpeg`, `.png`
* `.xls`, `.xlsx`
## File Size Limits
* Maximum supported file size: **30 MB per attachment**
Files exceeding this limit may not be available for export.
## Multiple Attachments
A single expense may contain multiple attachments.
The Export Item returns all associated files individually.
## Platform Behaviour
* Attachments remain linked to their originating expense.
* Download URLs are time-limited and should be retrieved promptly.
* File ordering is preserved as uploaded in Pleo.
***
## What Comes Next?
To understand how mapped data is finalised in the Accounting System, see:
* [Accounting Periods](/docs/current/platform/exports/accounting-periods)
* [Posting Behaviour](/docs/current/platform/exports/posting-behaviour)
For account assignment concepts, see:
* [Accounts Mapping](/docs/current/platform/exports/accounts-mapping)
***
## Related Reading
* [Integration Design - Attachment Handling](/docs/current/integration-design/exports/integration-design-exports-attachment-handling)
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Accounts Payable Bookkeeping
Source: https://developers.pleo.io/docs/current/platform/exports/bookkeeping-methods-accounts-payable
Accounts Payable (AP) bookkeeping separates vendor liabilities from payments, supporting accurate reconciliation, auditability, and financial clarity in Accounting Systems.
Pleo supports Accounts Payable bookkeeping for Export Items that require tracking **vendor liabilities independently from payments**, unlike Journal Entry bookkeeping where expenses are recorded directly in the general ledger.
Each expense may generate:
* **Invoice entry** – represents the liability owed to a vendor
* **Payment entry** – represents the actual payment
This method enables:
* Accurate reconciliation of vendor invoices and payments
* Clear tracking of refunds, chargebacks, and credit notes
* Handling of split expenses with multiple categories or tax treatments
* Vendor-specific or single Accounts Payable GL account assignment
By understanding these principles, developers can anticipate **how exported expenses will appear in the Accounting System**, and how AP bookkeeping interacts with other export capabilities.
## Core Principles
### Invoice vs Payment Separation
* Each expense produces an **invoice entry** representing the vendor liability, and a **payment entry** representing the actual payment.
* Invoice and payment entries may be recorded independently and at different times.
* Many Accounting Systems provide **Invoice Objects** (e.g., Purchase Invoices in Business Central) to record liabilities separately from journal entries.
* Pleo exports create invoice entries in these objects, while payment entries may be applied by:
* Marking the invoice object as paid, and/or
* Recording a payment in a journal, depending on the Accounting System’s capabilities.
### Vendor Assignment
* Each expense must be linked to a **vendor**, which can be:
* **Supplier** – external company providing goods or services
* **Employee** – when an employee covers an expense out-of-pocket; the reimbursement is treated as the payment entry
* Correct vendor assignment ensures invoice and payment entries are recorded accurately.
### Accounts Payable GL Account
* All vendor liabilities are tracked through a dedicated **Accounts Payable account**.
* This account can be configured as:
* Vendor-specific, or
* A single account for all vendors, depending on the Accounting System.
### Refunds and Chargebacks
* Refunds and chargebacks are recorded as **credit notes**, acting as the inverse of invoices.
* Credit notes allow separate tracking of returned funds versus original invoices.
* Pleo supports exporting credit notes for applicable expenses if the Accounting System provides this functionality.
### Split Expenses
* Expenses with multiple categories or tax treatments are recorded as **separate lines** on the invoice.
* Payments may still be recorded as a single entry reflecting the total amount.
* This ensures both detailed accounting records and accurate reconciliation.
## Relationship to Other Export Concepts
Accounts Payable bookkeeping interacts with other export capabilities during **per-item processing**:
* **Accounts Mapping** – determines which GL accounts are debited and credited for invoices, payments, and credit notes
* **Data Mapping** – ensures amounts, dates, dimensions, identifiers, and VAT/tax information are recorded correctly
* **Attachment Handling** – links receipts and supporting documents to invoice, payment, or credit note entries
* **Accounting Periods** – assigns entries to the correct accounting period
* **Posting Behaviour** – determines whether entries are created as drafts or finalised
Understanding these relationships helps developers anticipate **how AP bookkeeping integrates into the Export Lifecycle**.
***
## What Comes Next?
To understand how AP bookkeeping is exported in practice, see:
* [Accounts Mapping](/docs/current/platform/exports/accounts-mapping)
* [Data Mapping](/docs/current/platform/exports/data-mapping)
* [Attachment Handling](/docs/current/platform/exports/attachments)
* [Accounting Periods](/docs/current/platform/exports/accounting-periods)
* [Posting Behaviour](/docs/current/platform/exports/posting-behaviour)
For alternative bookkeeping methods, see:
* [Journal Entry Bookkeeping](/docs/current/platform/exports/bookkeeping-methods-journal-entry)
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [Bookkeeping Methods Overview](/docs/current/platform/exports/bookkeeping-methods-overview)
* [Integration Design - Accounts Payable Handling](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-accounts-payable)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Journal Entry Bookkeeping
Source: https://developers.pleo.io/docs/current/platform/exports/bookkeeping-methods-journal-entry
Journal Entry bookkeeping is the most fundamental method for recording financial transactions in an Accounting System.
When expenses are exported from Pleo using this method, each expense is recorded directly in the general ledger as a **journal entry**, following standard double-entry bookkeeping principles.
This method represents expenses as accounting movements between accounts rather than vendor invoices.
## What is a Journal Entry?
A journal entry represents a financial transaction recorded in the general ledger.
Although accounting systems may structure entries differently, a journal entry typically includes:
* Transaction date
* Monetary amount
* Debit account(s)
* Credit account(s)
* Tax or VAT information
* Descriptive notes or references
* Supporting identifiers for reconciliation
Journal entries provide the accounting record from which financial reports are generated.
## Double-Entry Bookkeeping
Journal entries follow the **double-entry bookkeeping principle**, meaning every transaction affects at least two accounts:
* One or more **debit entries**
* One or more **credit entries**
The total value of debits must always equal the total value of credits.
For exported Pleo expenses, this typically means:
* The **expense account** is debited
* A funding or liability account is credited (for example, wallet or reimbursement account)
The exact accounts used depend on the organisation’s configuration and account mappings.
## How Pleo Uses Journal Entries
When Journal Entry bookkeeping is selected:
* Each Export Item is recorded as a **journal entry** in the Accounting System, representing the financial transaction in a double-entry format (debits = credits).
* The expense amount, funding source, and tax/VAT data are mapped to the configured GL accounts according to the organisation’s accounting configuration.
* Supporting attachments (e.g., receipts) can be linked to the journal entry if the Accounting System supports it.
* The exact structure of the journal entry (single-line or multi-line) may vary depending on the Accounting System, but the accounting meaning remains consistent.
All journal entries are created during the **per-item processing stage** of the Export Lifecycle, ensuring each expense is individually tracked and reconcilable.
## When Journal Entry Bookkeeping is Used
Journal entries are commonly used when:
* Expenses are posted directly to the general ledger
* Vendor invoice tracking is not required
* The Accounting System operates primarily through ledger postings
* Simpler bookkeeping workflows are preferred
Additionally, some transaction types are **always recorded as journal entries**, regardless of configuration, including:
* Wallet loads
* Wallet unloads
* Balance amendments
These transactions do not represent vendor liabilities and therefore do not use Accounts Payable workflows.
## Variations Across Accounting Systems
Different Accounting Systems may represent journal entries differently. For example, an entry may:
* Appear as a single consolidated record containing debit, credit, and tax data, or
* Consist of multiple lines representing separate debit, credit, and tax postings.
Despite structural differences, the underlying accounting principle remains the same.
Integrations should therefore focus on preserving the **accounting meaning** rather than matching a specific system structure.
## Relationship to Other Export Concepts
Journal Entry bookkeeping works together with several other export capabilities:
* **Accounts Mapping**: determines which GL accounts are debited and credited
* **Data Mapping**: ensures amounts, dates, dimensions, and identifiers are recorded correctly
* **Attachment Handling**: links receipts and supporting documents
* **Posting Behaviour**: determines whether entries are created as drafts or finalised
These concepts are applied during the per-item processing stage of the Export Lifecycle.
***
## What Comes Next?
To understand how journal entries are configured and supported within exports, see:
* [Accounts Mapping](/docs/current/platform/exports/accounts-mapping)
* [Data Mapping](/docs/current/platform/exports/data-mapping)
* [Attachment Handling](/docs/current/platform/exports/attachments)
* [Posting Behaviour](/docs/current/platform/exports/posting-behaviour)
For invoice-based bookkeeping, see:
* [Accounts Payable Bookkeeping](/docs/current/platform/exports/bookkeeping-methods-accounts-payable)
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [Bookkeeping Methods Overview](/docs/current/platform/exports/bookkeeping-methods-overview)
* [Integration Design - Journal Entry Handling](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-journal-entry)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Bookkeeping Methods Overview
Source: https://developers.pleo.io/docs/current/platform/exports/bookkeeping-methods-overview
Pleo supports multiple bookkeeping methods for recording exported expenses in an Accounting System. Each Export Item includes a bookkeeping configuration that determines how it should be recorded.
Understanding these methods helps developers and integrators correctly configure integrations, map accounts and data, and ensure exported expenses are accurate, reconcilable, and compliant with accounting standards.
## Supported Bookkeeping Methods
### Journal Entry
* Expenses are recorded directly as [journal entries](/docs/current/platform/exports/bookkeeping-methods-journal-entry) in the Accounting System.
* Each entry follows standard **double-entry bookkeeping**, affecting a debit and a credit account.
* Suitable for organisations that post expenses directly to the general ledger without vendor invoice tracking.
### Accounts Payable
* Expenses are recorded using an [Accounts Payable](/docs/current/platform/exports/bookkeeping-methods-accounts-payable) workflow.
* Tracks **vendor liabilities separately from payments**:
* **Invoice entry**: records the amount owed to a vendor
* **Payment entry**: records the settlement of that invoice
* Useful for organisations requiring detailed vendor reconciliation, reporting, and invoice management.
### Default Behaviour
* Certain transaction types, such as **wallet loads**, **wallet unloads**, and **balance amendments**, are always recorded as **journal entries**, even if no explicit bookkeeping method is provided.
* This ensures consistency for transactions that do not involve vendor invoices or accounts payable tracking.
## Choosing the Right Method
* **Journal Entry** is simpler and works well if your Accounting System does not require separate vendor tracking.
* **Accounts Payable** is required if your Accounting System expects invoices and payment tracking, or if detailed vendor reconciliation is needed.
Some integrations may use a **combination of methods** depending on transaction type:
* Wallet top-ups, balance adjustments → always journal entries
* Vendor expenses → journal entry or accounts payable based on configuration
***
## What Comes Next?
* [Journal Entry Bookkeeping](/docs/current/platform/exports/bookkeeping-methods-journal-entry)
***
## Related Reading
* [Accounts Payable Bookkeeping](/docs/current/platform/exports/bookkeeping-methods-accounts-payable)
* Integration Design - [Determine Bookkeeping Method](/docs/current/integration-design/exports/integration-design-exports-bookkeeping-method-resolution)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
# Data Mapping
Source: https://developers.pleo.io/docs/current/platform/exports/data-mapping
Consistent and complete data mapping ensures that expenses exported from Pleo are accurately recorded, traceable, and easy to reconcile in the Accounting System.
After the **bookkeeping method** determines the accounting structure and **accounts mapping** assigns debit and credit accounts, **data mapping** defines **which transactional information populates those accounting records**.
In other words:
* Bookkeeping method decides **what structure is created**
* Accounts mapping decides **which accounts are used**
* Data mapping decides **what information fills the entry**
Data mapping links expense data from Pleo to corresponding fields in the Accounting System (AS), ensuring exported entries contain the information required for reporting, auditing, and reconciliation.
This processing occurs during **per-item processing** in the Export Lifecycle when Export Items are recorded in the Accounting System.
Correct data mapping ensures exported expenses are:
* Recorded accurately
* Consistent with user configuration
* Traceable between Pleo and the Accounting System
* Suitable for reporting and reconciliation workflows
## What Data Mapping Represents
Data mapping transfers **transaction context**, not accounting structure.
While accounts mapping determines *where postings occur*, data mapping determines:
* amounts
* dates
* identifiers
* classifications
* supporting metadata
These values allow users and accountants to understand **what the transaction represents**, not just where it was posted.
## Definitions of Common Data Points
### Expense GL Account (Category)
An account used to categorise specific types of expenses incurred by a business.
Examples:
* Rent
* Office Supplies
* Travel & Accommodation
This account determines where the expense is recorded in the general ledger and is provided on each Export Item.
### Total Expense Amount in Pleo Wallet Currency
The total monetary value of the expense expressed in the Pleo Wallet currency.
* Journal entries exported from Pleo are represented in wallet currency.
* It is assumed that the Accounting System’s base currency matches the wallet currency.
### Accounting Dimension
Additional classification applied to expenses to support reporting and analysis.
* Assigned in Pleo using Tags
Commonly mapped to:
* cost centres
* departments
* projects
Dimensions enable organisations to analyse spending beyond GL account level.
### Expense Date
The date on which the transaction occurred.
The Accounting System uses this date to determine the accounting period in which the expense is recorded.
### Unique Expense Identifier (Reconciliation ID)
A unique identifier assigned to each expense in Pleo.
* Must be recorded on the corresponding accounting entry
* Enables reliable reconciliation between Pleo and the Accounting System
* Allows users to trace exported entries back to their originating expense
### Unique IDs of Pocket Expenses Related to a Reimbursement
A reimbursement may relate to multiple out-of-pocket expenses.
To support reconciliation:
* The reimbursement entry should include the unique IDs of all related out-of-pocket expenses.
* This allows users to trace reimbursed amounts back to individual expenses recorded in Pleo.
### Internal Document Number
A unique identifier automatically assigned by the Accounting System.
* Generated by the Accounting System
* Visible to users
* Used for internal referencing and navigation
### Attendees
Represents participants present at the time of the spend.
This information may be required for:
* compliance
* auditing
* expense policy enforcement
Availability depends on Accounting System capabilities.
## Relationship to Other Export Concepts
Data mapping operates alongside other export capabilities during per-item processing:
* **Bookkeeping Method** — defines the accounting structure created
* **Accounts Mapping** — assigns debit and credit accounts
* **Attachment Handling** — links receipts and supporting documentation
* **Accounting Periods** — determines posting period assignment
* **Posting Behaviour** — controls whether entries are drafts or finalised
Together, these concepts ensure exported expenses are both **accounting-correct and operationally usable**.
***
## What Comes Next?
To understand how mapped data is finalised in the Accounting System, see:
* [Attachments](/docs/current/platform/exports/attachments)
* [Accounting Periods](/docs/current/platform/exports/accounting-periods)
* [Posting Behaviour](/docs/current/platform/exports/posting-behaviour)
For account assignment concepts, see:
* [Accounts Mapping](/docs/current/platform/exports/accounts-mapping)
***
## Related Reading
* [Export Lifecycle](/docs/current/platform/exports/lifecycle)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Export Lifecycle
Source: https://developers.pleo.io/docs/current/platform/exports/lifecycle
The **Export Lifecycle** describes how Pleo moves expenses and related attachments from the platform into an Accounting System, respecting user bookkeeping configuration, accounting periods, and posting preferences.
Understanding this lifecycle helps developers conceptualise **what Pleo expects, how data is structured, and the order of operations** before implementation.
**This lifecycle ensures exported expenses are accurate, reconcilable, and compliant with company bookkeeping standards.**
## High-Level Process
## High-Level Steps
### 1. Export Job Creation
* Expenses that are ready for export are grouped into an Export Job.
* Export Jobs are queued in Pleo for processing by an integration.
* Only approved and accounting-ready expenses are included.
See: [How to Queue Export Items in Pleo's Web App](/docs/current/how-tos/accounting-integrations/how-to-queue-export-items-in-ui)
### 2. Export Job Detection
* Integrations detect pending Export Jobs using **webhooks** (preferred) or **scheduled polling**.
* Jobs are processed sequentially (oldest first) to maintain consistency.
* An Export Job is marked as **started** when processing begins.
See: [How to Detect and Start Export Jobs for Processing](/docs/current/how-tos/accounting-integrations/how-to-detect-and-start-export-jobs-for-as-erp-processing)
### 3. Pre-Export Validation
* The integration ensures the Accounting System is available and ready to accept entries.
* If unavailable, the Export Job is **failed** with actionable error information.
See: [How to Perform Pre-Export Validation](/docs/current/how-tos/accounting-integrations/how-to-perform-pre-export-validation-for-as-erp-processing)
### 4. Export Item Processing
* Individual **Export Items** (expenses) are fetched from the Export Job.
* Each item is validated for required configuration and data.
* Valid items proceed through the **per-item processing block**.
* **Individual item failures do not fail the entire job; each item is tracked separately to allow partial job success.**
See:
* [How to Retrieve Export Job Items for Processing](/docs/current/how-tos/accounting-integrations/how-to-retrieve-export-job-items-for-as-erp-processing)
* [How to Fetch Export Item Data for Processing](/docs/current/how-tos/accounting-integrations/how-to-fetch-export-item-data-for-as-erp-processing)
## Per-Item Processing
Each Export Item goes through the following steps:
### 5. Bookkeeping Method
* Expenses are recorded using one of two methods:
* **Journal Entries**: simple double-entry transactions
* **Accounts Payable**: invoice and payment tracking for vendors
* Certain transaction types (wallet loads, balance amendments) are always journal entries.
See: [How to Determine the Bookkeeping Method](/docs/current/how-tos/accounting-integrations/how-to-determine-the-bookkeeping-method-for-as-erp-processing)
### 6. Accounts & Data Mapping
* Exported expenses are mapped to the correct **GL Accounts**:
* Wallet, Out-of-Pocket, Contra, Expense Category, Accounts Payable
* Data mapping ensures all **mandatory**, **invoice-specific**, and **optional** datapoints are correctly recorded:
* Amounts, dates, tax codes, dimensions, unique identifiers, VAT/tax information
See:
* [How to Apply Accounts Mapping for Export Items](/docs/current/how-tos/accounting-integrations/how-to-determine-accounts-mapping-for-as-erp-processing)
* [How to Apply Data Mapping](/docs/current/how-tos/accounting-integrations/how-to-apply-data-mapping-for-as-erp-processing)
### 7. Attachment Handling
* Receipts and supporting documents are linked to the corresponding journal entry, invoice, or credit note.
* Multiple attachments are supported if the Accounting System allows.
See: [How to Transfer Attachments](/docs/current/how-tos/accounting-integrations/how-to-transfer-attachments-for-as-erp-processing)
### 8. Accounting Period Assignment
* Expenses are assigned to the correct accounting period, even if the period is closed or uses a custom calendar.
See: [How to Assign Accounting Periods](/docs/current/how-tos/accounting-integrations/how-to-assign-accounting-periods-for-as-erp-processing)
### 9. Posting Behaviour
* Entries may be recorded as **drafts** for review or **finalised** automatically, depending on configuration.
See: [How to Apply Posting Behaviour](/docs/current/how-tos/accounting-integrations/how-to-apply-posting-behaviour-for-as-erp-processing)
## Status Updates
### 10. Update Export Item Status
* Each Export Item is updated with its success or failure status.
See: [How to Update Export Items](/docs/current/how-tos/accounting-integrations/how-to-update-export-items-for-as-erp-processing)
### 11. Update Export Job Status
* Once all items are processed, the Export Job is updated with an overall status reflecting the combined results:
* **Completed:** all Export Items succeeded
* **Completed with Errors:** some items failed
* **Failed:** all items failed
See: [How to Update & Complete the Export Job](/docs/current/how-tos/accounting-integrations/how-to-update-and-complete-export-job-for-as-erp-processing)
***
## What Comes Next?
The following pages provide a deeper explanation of each step and the platform capabilities that support it:
* [Bookkeeping Methods Overview](/docs/current/platform/exports/bookkeeping-methods-overview)
* [Journal Entry Bookkeeping](/docs/current/platform/exports/bookkeeping-methods-journal-entry)
* [Accounts Payable Bookkeeping](/docs/current/platform/exports/bookkeeping-methods-accounts-payable)
* [Accounts Mapping](/docs/current/platform/exports/accounts-mapping)
* [Data Mapping](/docs/current/platform/exports/data-mapping)
* [Attachments](/docs/current/platform/exports/attachments)
* [Accounting Periods](/docs/current/platform/exports/accounting-periods)
* [Posting Behaviour](/docs/current/platform/exports/posting-behaviour)
***
## Related Reading
* [Connection and Authorisation Overview](/docs/current/integration-design/auth/integration-design-auth-overview)
* [OAuth 2.0 Access to Staging Workflow Guide](/docs/current/guides/environment-access/staging-oauth-workflow)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
# Posting Behaviour
Source: https://developers.pleo.io/docs/current/platform/exports/posting-behaviour
Pleo does not control or configure posting behaviour.
Each export provides the necessary expense data, and the integration must decide whether entries are created as **draft** or **finalised** in the connected Accounting System.
## Core Principle
Pleo provides:
* Expense data for export
The integration is responsible for:
* Determining posting state (draft or final)
* Mapping to Accounting System posting capabilities
* Applying any business rules related to posting
## Posting Modes
Most Accounting Systems support different posting states. The integration may choose to create:
### Draft Posting
Entries are created as editable records.
Typical characteristics:
* Entries can be reviewed and adjusted
* Changes can be made before final posting
* Often used to reduce risk before committing financial impact
### Finalised Posting
Entries are created as fully posted records.
Typical characteristics:
* Entries are non-editable
* Posting is considered complete
* Financial impact occurs immediately
## Posting Decision
The integration determines posting behaviour based on:
* Integration design choices
* Customer requirements
* Accounting System capabilities
Pleo does not provide or enforce a posting preference.
## Platform Behaviour
* No posting state is defined or enforced by Pleo
* No configuration for posting behaviour exists in Pleo
* Pleo does not validate Accounting System posting rules
* Final posting behaviour is determined during integration execution
***
## What Comes Next?
* [Learn how to implement exports from Pleo into an Accounting System](/docs/current/integration-design/exports/integration-design-exports-overview)
***
## Related Reading
* [Integration Design - Posting Behaviour](/docs/current/integration-design/exports/integration-design-exports-posting-behaviour)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Processing Workflow Guide](/docs/current/guides/accounting-system-processing-workflow-guide)
***
# Multi-Entity & API Scope
Source: https://developers.pleo.io/docs/current/platform/multi-entity/multi-entity-and-api-scope
Pleo supports organisations with multiple legal entities (companies).
When integrating with the Pleo API, it’s important to understand how **companies** and **organisations** are represented, and how this affects API access and data isolation.
This page explains Pleo’s multi-entity model and the API constraints that apply in these setups.
## Company vs Organisation
Pleo distinguishes between **companies** and **organisations**:
| Term | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Company** | A single legal entity in Pleo, identified by a `company_id`. All expenses, employees, wallets, and accounting exports belong to a specific company. |
| **Organisation** | A group of one or more companies, identified by an `organisation_id`. Organisations are used for administration and discovery, not accounting exports. |
**Example:**
* *Alexey Software Ltd* operates as a single company → one `company_id`
* *Love Crème Brûlée Group* operates subsidiaries in NL, DE, and BE → one `organisation_id`, multiple `company_id`s
## How Pleo Supports Multi-Entity Organisations
A single Pleo organisation may contain multiple companies with:
* Separate accounting configurations
* Separate employees and vendors
* Separate wallets and currencies
* Separate ERP or Accounting System integrations
Each company is treated as an **independent accounting boundary**.
## API Scope and Entity Isolation
The Pleo API enforces **strict company-level isolation**.
### Key rule
> **All accounting-related API calls are scoped to exactly one `company_id`.**
* You **cannot** perform accounting operations across multiple companies in a single request
* You **cannot** update or export data for multiple companies using an `organisation_id`
This applies to:
* Expense exports
* Master data sync (accounts, tax codes, vendors, tags)
* Other supported APIs that uses company\_id.
Each company must be handled independently.
## Authentication Scope in Multi-Entity Setups
Authentication credentials are also **company-scoped**.
### Accounting integrations
* API keys and OAuth 2.0 clients are created **per company**
* Credentials are valid **only for the company they were generated for**
* Multi-entity organisations require **separate credentials per company**
### Organisation-level access
* Organisation-scoped tokens may be used for:
* Discovering companies
* Automation and orchestration
* They **cannot** be used to bypass company-level isolation for accounting operations
## Practical Integration Implications
If you’re integrating Pleo for a multi-entity organisation:
* Treat each `company_id` as a separate integration instance
* Store and manage credentials per company
* Run exports, retries, and error handling per company
* Avoid assumptions that organisation-level access implies accounting access
This design is intentional and ensures:
* Accounting integrity
* Auditability
* Clear ownership of financial data
## What comes next?
* **Explore Employees and Roles**
Understanding how [employees and roles](/docs/current/platform/employees/employees-and-roles) are represented in Pleo is essential when designing integrations, configuring permissions, and automating workflows.
# Access & Permissions
Source: https://developers.pleo.io/docs/current/pleo-mcp/get-started/access-and-permissions
How company opt-in, user permissions, tool tiers, and security boundaries govern access to the Pleo MCP.
The Pleo MCP uses a layered permission model. Company admins control whether users can connect to the Pleo MCP. What AI agents can access through the MCP is then governed by each user's existing Pleo permissions and their configuration of their AI client.
## For Admins
### Enabling MCP Access
MCP access is **off by default**. An admin must enable it before any user in the organisation can connect an AI client.
**To enable MCP access:**
1. Go to **Settings → General → Pleo AI** in the Pleo settings
2. Under the **MCP** section, enable **Pleo MCP access**
MCP access is configured per entity in Pleo. If your organisation has multiple entities, enable it separately for each entity you want to use with MCP.
### Disabling MCP Access
Admins can disable MCP access for the entire organisation by turning off the toggle in **Settings → General → Pleo AI**.
### Staging Environment
The staging MCP server mirrors the production permission model. MCP access must also be enabled per entity in your staging Pleo account before connecting.
See [How to Install the Pleo MCP as a Custom MCP: Using Staging](/docs/current/pleo-mcp/how-tos/installation/custom-mcp#using-staging) for setup steps.
## For Users
### User Permission Model
The MCP server does not grant any new permissions. An agent acting on behalf of a user can only do what that user can already do in the Pleo product.
* A spender can read and update their own expenses
* An agent cannot access data the connecting user cannot see in the Pleo app
Users can configure what tools require user approval within their AI client. Write tools that require approval will prompt the user to confirm the intended change before the tool executes.
### Working Across Multiple Entities
If you have access to more than one Pleo entity, the agent can work in any of them without you reconnecting or re-authorising.
* **Each request targets one entity at a time.** There is no "all entities at once" mode. The agent cannot combine data from two entities within a single request.
* **Your default entity is used unless you say otherwise.** This is the entity you connected during setup. To work in a different one, name it in your request, for example "show my expenses in Beta Ltd".
* **Naming the entity is not sticky.** If you want to stay in a non-default entity across several requests, keep referring to it, or tell the agent to keep working in that entity.
* **Each entity needs its own admin opt-in.** An entity where MCP access hasn't been enabled is not available to the agent, even if you have access to it in the Pleo app.
Your permissions are resolved per entity. If you are an owner in one entity and a spender in another, the agent can only do what your role in that specific entity allows.
### Revoking Your Access
Users can disconnect their own AI client's MCP access. See [How to Revoke Access to the Pleo MCP](/docs/current/pleo-mcp/how-tos/uninstallation/revoke-access) for the full steps.
## Audit Trail
Any action taken or data updated in Pleo are recorded in Pleo's activity and audit logs. The actions are visible in the Activity tab for the relevant expense.
## Security Boundaries
* **No payment actions.** The MCP does not support wallet top-ups, reimbursement payouts, invoice payments, or any autonomous payment decisions. SCA and regulatory controls remain intact.
* **No permission elevation.** An agent cannot access data or take actions beyond the connected user's existing Pleo permissions.
***
## What Comes Next?
* [Pleo MCP Enablement: Choose Your AI Platform](/docs/current/pleo-mcp/how-tos/enablement/overview#choose-your-ai-platform)
* [Install the Pleo MCP: Choose Your AI Tool](/docs/current/pleo-mcp/how-tos/installation/overview#choose-your-ai-tool)
***
## Related Reading
* [FAQs](/docs/current/pleo-mcp/get-started/faq)
***
# FAQs
Source: https://developers.pleo.io/docs/current/pleo-mcp/get-started/faq
Answers to common questions about the Pleo MCP.
Browse frequently asked questions about the Pleo MCP, grouped by topic.
Who can use the Pleo MCP, what it can access, and how to manage or revoke that access.
How data flows between Pleo and your AI tool, and what the MCP can and can't touch.
What the Pleo MCP supports today, and how it compares to the Pleo API.
Connecting custom LLMs, agents, and staging accounts to the Pleo MCP.
Whether using the Pleo MCP costs extra.
What to do when the Pleo MCP isn't behaving as expected.
***
## Permissions & Access
### What permissions does the Pleo MCP have?
Actions taken through the Pleo MCP follow the same permissions as the connected user. The agent can never do more in Pleo than the user who connected it can do. See [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions) for details.
### How does the Pleo MCP work with multiple entities?
If you have access to more than one Pleo entity, you can ask the agent to work in any of them without reconnecting. Each request targets a single entity at a time, so name the entity you mean if it isn't your default one. MCP access is enabled per entity, so an admin must turn it on for each entity you want to use. See [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions#working-across-multiple-entities).
### Can I enable the Pleo MCP for only specific users?
Not yet. MCP access is currently enabled or disabled for an entire entity at once. Per-user access controls are planned for a future release.
### How do I revoke access for an agent or MCP client?
Go to **My Account → Security and Devices → MCP Connections** in the Pleo app to see connected MCP clients and revoke access. See [How to Revoke Access to the Pleo MCP](/docs/current/pleo-mcp/how-tos/uninstallation/revoke-access) for step-by-step instructions.
***
## Data & Security
### How is data processed between Pleo and the AI tool?
Pleo makes data available through the Pleo MCP, and any data read through the MCP is passed to your connected agent, AI tool, and model provider. Pleo also has access to all data sent to Pleo through the Pleo MCP tools.
### Could the beta Pleo MCP accidentally impact my Pleo setup?
The Pleo MCP cannot change company setup or configuration. It only reads and writes data on individual expenses, and any expense data updated through the MCP is visible in Pleo's activity logs.
***
## Capabilities & Use Cases
### What use cases are covered by the Pleo MCP?
The Pleo MCP can search for expenses and read or write data on individual expenses, including category, tags, tax codes, notes, attendees, and receipts. See [Pleo MCP Capabilities](/docs/current/pleo-mcp/get-started/pleo-mcp-capabilities) for the full list of what's supported today.
### What's the difference between the Pleo MCP and the Pleo API?
The Pleo MCP is optimised for AI agents: connect it directly to a client like Claude, and the agent decides which tools to call and when. The Pleo API is designed for code to call directly with explicit, predetermined logic.
### Can I use the Pleo MCP to attach receipts from email, Slack, or Drive?
The Pleo MCP supports adding receipts to Pleo. If your agent has access to receipts, for example in an email inbox, Slack, or Drive, you can ask it to match them to the right expenses and attach them. See [Example Use Cases](/docs/current/pleo-mcp/get-started/use-cases) for an example prompt.
***
## Setup & Compatibility
### Can I use the Pleo MCP with a custom LLM or agent?
Yes. You can connect the Pleo MCP to any agent tool or harness that supports custom MCP servers. See [How to Install the Pleo MCP as a Custom MCP](/docs/current/pleo-mcp/how-tos/installation/custom-mcp). Your organisation may limit which MCPs you're allowed to connect.
### Can I use the Pleo MCP with a Pleo Staging account?
Yes. You must first enable MCP access under **Settings → General → Pleo AI** for each entity in your staging Pleo account. Then connect using the staging URL: `https://mcp.staging.pleo.io/mcp`. For the full staging setup, see [How to Install the Pleo MCP as a Custom MCP](/docs/current/pleo-mcp/how-tos/installation/custom-mcp#using-staging).
***
## Pricing
### Does using the Pleo MCP cost extra?
No. The Pleo MCP is included with eligible Pleo pricing plans at no extra charge.
***
## Troubleshooting
### ChatGPT Business
#### Why does ChatGPT say the connector name already exists?
App names must be unique within the workspace, and another app is already using that name. Choose a different name to continue. See [How to Install the Pleo MCP in ChatGPT Business](/docs/current/pleo-mcp/how-tos/installation/chatgpt-business) for instructions.
#### Why doesn't the draft appear under Apps → Drafts?
Creating a custom app isn't instant: it can take a few seconds for the draft to appear. Wait a moment, then refresh the **Apps → Drafts** list. See [How to Install the Pleo MCP in ChatGPT Business](/docs/current/pleo-mcp/how-tos/installation/chatgpt-business) for instructions.
#### Why can't members find the Pleo MCP in Settings → Plugins?
Publishing an app only makes it **Available**; it isn't added to members' workspace until it's set to **Installed**. Check that **Workspace settings → Plugins → Pleo MCP → Availability** is set to **Installed**, not only **Available**. See [How to Install the Pleo MCP in ChatGPT Business](/docs/current/pleo-mcp/how-tos/installation/chatgpt-business) for instructions.
#### Why doesn't the Pleo MCP consent screen load, or why does the redirect stall?
This is usually a temporary glitch in the OAuth handoff between ChatGPT and Pleo, rather than a permissions or configuration issue. Start the connection again from **Settings → Plugins → Connect**. See [How to Install the Pleo MCP in ChatGPT Business](/docs/current/pleo-mcp/how-tos/installation/chatgpt-business) for instructions.
#### Why isn't ChatGPT using the Pleo MCP?
ChatGPT only uses connectors that are enabled for the current conversation, even after an admin has installed the Pleo MCP and you've authorised it. Select the connector for that conversation: in the chat composer, choose **+ → Connect plugins**, enable the Pleo MCP, then ask again. See [How to Install the Pleo MCP in ChatGPT Business](/docs/current/pleo-mcp/how-tos/installation/chatgpt-business) for instructions.
***
## Related Reading
* [Pleo MCP Overview](/docs/current/pleo-mcp/get-started/pleo-mcp-overview)
* [Example Use Cases](/docs/current/pleo-mcp/get-started/use-cases)
* [How to Install the Pleo MCP in ChatGPT Business](/docs/current/pleo-mcp/how-tos/installation/chatgpt-business)
# Pleo MCP Capabilities
Source: https://developers.pleo.io/docs/current/pleo-mcp/get-started/pleo-mcp-capabilities
A breakdown of the expense actions the Pleo MCP supports today, from search and review to export preparation.
## What You Can Do Today
The Pleo MCP covers the **processing of expenses** in Pleo. It can search for and retrieve data about individual expenses, and find expenses that are still missing details. From there, it can complete those expenses by setting categories and tags, splitting them across categories and tax codes, adding notes and attendees, and attaching receipts. It can also surface the expenses waiting on you for review and act on your decision, then add expenses to the export queue once they're ready.
* Manage your expenses. Find expenses missing details and update data in Pleo
* Review and approve expenses
* Check expense data and prepare for bookkeeping and export from Pleo
* Work across multiple entities, if you have access to more than one
## Multiple Entities
If you have access to more than one Pleo entity, you can ask the agent to work in any entity that has MCP access enabled, without reconnecting. Each request targets a single entity at a time. See [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions#working-across-multiple-entities).
## Read and Write Actions
The Pleo MCP exposes both read tools, for searching and retrieving expense data, and write tools, for updating it. AI clients like Claude and ChatGPT require user approval before executing write actions by default, though this can be configured in the AI tool.
***
## What Comes Next?
* [Example Use Cases](/docs/current/pleo-mcp/get-started/use-cases)
***
## Related Reading
* [FAQs](/docs/current/pleo-mcp/get-started/faq)
* [Install the Pleo MCP: Choose Your AI Tool](/docs/current/pleo-mcp/how-tos/installation/overview#choose-your-ai-tool)
* [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions)
***
# Pleo MCP Overview
Source: https://developers.pleo.io/docs/current/pleo-mcp/get-started/pleo-mcp-overview
What the Pleo MCP is, how it works, and where to go next to install it or learn about permissions.
## What Is Pleo MCP?
The Pleo MCP server connects your AI assistant or agent to your Pleo data, so you can manage Pleo by talking to your assistant, or let an AI agent manage it on your behalf.
MCP, the Model Context Protocol, is an open standard for connecting AI assistants to external tools and data. Unlike Pleo's REST APIs, which are built for code to call, MCP tools are designed for agents to discover and call at runtime: the agent reads each tool's description, decides which ones to call and in what order, and acts on what it finds.
## Data Processing
Your expense data, including the names of colleagues on your expenses, will be processed by your AI provider under their terms and privacy policy. The AI provider may be based outside the UK or EU. Check their privacy policy if you want to understand how they handle your data.
The agent's responses are not generated by Pleo. Review anything significant before acting on it.
Ensure you review and understand how your AI provider processes your data.
## How It Works
When you ask an AI agent to help with your Pleo expenses, the agent connects to the Pleo MCP server, which translates the request into the appropriate Pleo actions and returns the results. The agent can only do what you are already permitted to do in Pleo: it inherits your permissions.
## What You Can Do Today
The Pleo MCP currently focuses on the **processing of expenses**, from finding and updating incomplete expenses to preparing them for bookkeeping and export.
Full breakdown of what's supported today.
## Permissions
The Pleo MCP inherits the authenticated user's Pleo permissions. The agent can only access data and take actions the user is already permitted to perform. MCP access must be enabled by a company admin before users can connect.
How company opt-in works, what the agent can and cannot do, and how to manage access.
***
## What Comes Next?
Connect, configure, or remove the Pleo MCP. Choose the guide that matches your setup.
Admin steps to allow the Pleo MCP in your AI platform.
Step-by-step instructions for connecting your AI assistant to Pleo
Disconnect the Pleo MCP from Pleo or your AI client.
Ready-to-paste prompts for common workflows
Answers to common questions about permissions, data, and setup.
***
# Example Use Cases
Source: https://developers.pleo.io/docs/current/pleo-mcp/get-started/use-cases
Common tasks customers can solve by asking their connected AI assistant, with example prompts for each.
Once the Pleo MCP is connected to your AI client, you can ask it to handle these tasks directly. Paste any of the example prompts below into your chat to try it.
An agent can only do what the connecting user can already do in Pleo, so the tasks available to you depend on your role. See [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions) for the full permission model.
## For Finance Teams
Company-wide tasks for admins and bookkeepers closing the books, chasing spenders, and reviewing spend.
| Use Case | Example Prompt | What the Agent Does |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **See what's blocking the close** | "Build me a dashboard of everything blocking our month-end close." | Pulls together the expenses that aren't export-ready — missing receipts, missing info, missing categories, and pending approvals — and groups them into a single view of what's outstanding. |
| **Know who to chase** | "Show me the spenders and approvers with the highest value of pending expenses." | Ranks spenders by outstanding expenses and approvers by pending reviews, by both count and value, so you can prioritise who to chase or escalate to a manager. |
| **Remind people with missing receipts and data to complete their expenses** | "Find everyone with incomplete expenses this week and draft a Slack message to each of them listing what's outstanding." | Groups outstanding expenses by spender and drafts a personalised message per person citing their specific pending expenses, leaving you to review and send. *This can be set to run on a schedule in your AI assistant.* |
| **Chase pending approvals** | "Draft a Slack reminder for every approver with expenses that have been waiting more than a week." | Finds expenses still awaiting review, groups them by approver, and drafts a reminder for each one listing the specific expenses they're holding up. |
| **Re-categorise expenses in bulk** | "Recategorise all the ride-hailing expenses from last month as Travel." | Finds every matching expense across the company, proposes the correct category for each against your chart of accounts, and applies the changes in one go once you confirm. Useful when spenders don't know the category taxonomy and you'd otherwise fix them one by one. |
| **Keep vendor coding consistent** | "Categorise this month's expenses from our recurring vendors the way we've coded them before." | Looks at how each vendor's expenses have been categorised and tagged previously and applies the same coding to new expenses from that vendor for you to confirm. |
| **Find and upload missing receipts** | "Find the receipts for my expenses that are missing one and attach them in Pleo." | Identifies which expenses are missing a receipt, retrieves the matching receipt from your email inbox or the vendor's billing portal, checks the amount against the expense, and uploads it to Pleo. |
| **Scan for anomalies and miscoded spend** | "Review this month's expenses for anything unusual or likely miscoded." | Scans company spend for outliers, duplicates, and expenses whose category doesn't match the merchant, then returns a flagged list with the reasoning behind each one. |
| **Review and approve expenses** | "Show me what's waiting for my approval and flag anything that looks off." | Lists your pending reviews with the context behind each expense, then approves or rejects them once you decide. |
| **Queue export-ready expenses** | "Which expenses are ready to export, and can you queue them?" | Checks which expenses have everything they need, then adds them to the export queue once you confirm. |
Some of these tasks combine the Pleo MCP with other tools in your AI client. Drafting Slack messages needs a Slack connection, and retrieving receipts from an inbox or a vendor portal needs email or browser access. The agent uses Pleo for the expense data and those tools for everything outside Pleo.
## For Spenders
Everyday tasks for anyone spending on a Pleo card and completing their own expenses.
| Use Case | Example Prompt | What the Agent Does |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Complete expenses before export** | "Show me my expenses with missing info and what needs updating." | Finds expenses missing a receipt, category, note, or tags, proposes fixes based on the merchant and description, and applies them once you confirm. |
| **Attach a receipt from email, Slack, or Drive** | "Here's the receipt for my taxi to the airport, can you attach it to that expense?" | Matches the receipt to the right expense and attaches it directly from the link, no downloading or uploading required. |
| **Categorise and tag a batch of expenses** | "Categorise and tag all my expenses from this trip." | Reviews a backlog of recent expenses and proposes categories and cost centre tags for you to confirm in bulk. |
| **Add attendees to a client meal** | "Add John and Sarah from Acme Corp as attendees to my dinner with the client yesterday." | Checks attendees against your contact list, confirms details for anyone new, and adds them to the expense. |
| **Split an expense across categories** | "Split my hotel bill: the two nights as Travel and the dinner as Client Entertainment." | Splits the expense across multiple categories, tax codes, and tags, keeping the total intact. |
| **Find a specific expense** | "Find my expense for the hotel in Berlin last month." | Searches by merchant, note, or supplier and returns the full expense record, including amount, date, category, tags, and receipt status. |
Fetching a receipt from an email only works when the vendor's email contains a public link to the PDF or the email connector supports downloading attachments. Some email connectors can't download attachments directly. Fetching from a vendor's billing portal often requires the agent to have access to a web browser.
***
## What Comes Next?
* [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions)
***
## Related Reading
* [FAQs](/docs/current/pleo-mcp/get-started/faq)
* [Install the Pleo MCP: Choose Your AI Tool](/docs/current/pleo-mcp/how-tos/installation/overview#choose-your-ai-tool)
***
# How to Add Pleo MCP as a Custom Connector in ChatGPT
Source: https://developers.pleo.io/docs/current/pleo-mcp/how-tos/enablement/chatgpt
How a ChatGPT Business, Enterprise, or Edu workspace admin enables custom connectors so the Pleo MCP can be installed.
This page is for admins of a ChatGPT **Business**, **Enterprise**, or **Edu** workspace. Custom connectors are off by default on these plans until an admin enables them.
If you're on ChatGPT Plus or Pro, you can install the Pleo MCP yourself without any admin step. See [How to Install the Pleo MCP in ChatGPT](/docs/current/pleo-mcp/how-tos/installation/chatgpt).
Enabling Developer mode unlocks the option to create custom connectors. It doesn't install or authorise the Pleo MCP. A workspace admin still needs to publish and install it, and each member must authorise it themselves.
## Steps
Follow the steps below to enable custom connectors for your workspace.
1. Go to **Workspace Settings → Permissions & Roles → Connected Data**.
2. Enable **Developer mode** (or the equivalent **custom MCP connectors** permission) for the workspace, or for specific roles if you want to limit who can create connectors.
This setting has rolled out gradually. If you don't see it under Connected Data, check under **Connectors → Draft** or contact OpenAI support to confirm availability for your workspace.
3. Under **Admin → Apps**, review app permissions. Set the workspace default and any per-app overrides you need for the Pleo MCP once it's installed.
Custom connectors are now enabled for your workspace.
***
## What Comes Next?
* [How to Install the Pleo MCP in ChatGPT Business](/docs/current/pleo-mcp/how-tos/installation/chatgpt-business)
***
## Related Reading
* **OpenAI's documentation:** [Admin Controls, Security, and Compliance in apps (Enterprise, Edu, and Business)](https://help.openai.com/en/articles/11509118)
* [FAQs](/docs/current/pleo-mcp/get-started/faq)
* [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions)
***
# How to Add Pleo MCP as a Custom Connector in Claude
Source: https://developers.pleo.io/docs/current/pleo-mcp/how-tos/enablement/claude
How a Claude Team or Enterprise admin adds Pleo as an organisation-wide custom connector.
This page is for Claude for Work **Owners** and **Primary Owners** (or members with the Identity & Access custom role).
If you're an individual Claude Free, Pro, or Max user, you can add Pleo yourself. See [How to Install the Pleo MCP in Claude](/docs/current/pleo-mcp/how-tos/installation/claude).
Adding Pleo at the org level makes it available to members, but it doesn't connect their accounts. Each member must still sign in to Pleo and approve access themselves the first time they use it.
## Steps
Follow the steps below in the Claude Admin Console to add Pleo as a custom connector for your **organisation**.
1. Go to **Organization Settings → Connectors**.
2. Click **Add**, then choose **Custom** → **Web**.
3. Enter the Pleo MCP server URL:
```
https://mcp.pleo.io/mcp
```
No Advanced or Auth settings are needed.
| Property | Value |
| ------------------ | --------------------------------------- |
| **MCP URL** | `https://mcp.pleo.io/mcp` |
| **Transport** | Streamable HTTP |
| **Authentication** | OAuth 2.0 (no credentials to configure) |
4. Add the connector. It becomes available to everyone in your organisation.
5. Set **Tool permissions** for Pleo's tools: **Always allow**, **Needs approval**, or **Blocked**. Pleo recommends requiring approval for any tool that writes data.
Pleo is now available as a connector for your organisation.
See **Anthropic's documentation:** [Add a custom connector](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp#h_3d1a65aded)
***
## What Comes Next?
* [How to Install the Pleo MCP in Claude](/docs/current/pleo-mcp/how-tos/installation/claude)
***
## Related Reading
* **Anthropic's documentation:** [Add a custom connector](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp#h_3d1a65aded)
* [FAQs](/docs/current/pleo-mcp/get-started/faq)
* [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions)
***
# How to Add Pleo MCP as a Custom Connector in Codex
Source: https://developers.pleo.io/docs/current/pleo-mcp/how-tos/enablement/codex
Where org-level control over Pleo MCP access actually lives for Codex users, since Codex CLI has no admin console of its own.
This page covers where org-level control over Pleo MCP access actually lives for Codex, since Codex CLI has no admin console of its own.
Codex CLI reads its MCP server configuration from a file on each developer's own machine (`~/.codex/config.toml`), so there's no "add a custom connector" step for an admin to complete here.
Instead, control over custom connectors sits in one of the following places, depending on how your organisation runs Codex:
* **Using Codex through a ChatGPT Business, Enterprise, or Edu workspace:** your workspace admin controls custom connectors centrally. See [How to Add Pleo MCP as a Custom Connector in ChatGPT](/docs/current/pleo-mcp/how-tos/enablement/chatgpt).
* **Using Codex CLI directly on developer machines:** there's no product-level restriction. If your organisation needs to restrict outbound connections, this is a network-level control (for example, allowlisting `mcp.pleo.io` at the firewall or proxy) managed by your IT or security team, not a Codex setting.
Once your organisation's network and Pleo-side access allow it, individual developers can connect Codex to Pleo themselves. See [How to Install the Pleo MCP in Codex](/docs/current/pleo-mcp/how-tos/installation/codex).
***
## What Comes Next?
* [How to Install the Pleo MCP in Codex](/docs/current/pleo-mcp/how-tos/installation/codex)
***
## Related Reading
* **OpenAI's documentation:** [Model Context Protocol – Codex](https://developers.openai.com/codex/mcp)
* [FAQs](/docs/current/pleo-mcp/get-started/faq)
* [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions)
***
# How to Add Pleo MCP as a Custom Connector in Microsoft Copilot
Source: https://developers.pleo.io/docs/current/pleo-mcp/how-tos/enablement/copilot
How a Microsoft 365 tenant admin approves Pleo as an allowed MCP server for Copilot Studio agents.
This page is for **AI Administrators** or **Global Administrators** of a Microsoft 365 tenant.
Whether you need to do anything here depends on how your tenant is configured: many tenants allow makers to add MCP servers to Copilot Studio agents without any admin approval.
## Steps
Follow the steps below to approve Pleo for your tenant.
1. Approve Pleo in the Microsoft 365 admin center using the following details. See **Microsoft's documentation:** [Manage tools for agents in Microsoft 365 admin center](https://learn.microsoft.com/microsoft-365/admin/manage/manage-tools-for-agent)
| Property | Value |
| ------------------ | --------------------------------------- |
| **MCP URL** | `https://mcp.pleo.io/mcp` |
| **Transport** | Streamable HTTP |
| **Authentication** | OAuth 2.0 (no credentials to configure) |
2. Also check connector policies in the **Power Platform admin center → Environment Groups → Rules**, or **Security → Advanced Connector Policies**. Some tenants govern this instead, or in addition, through these settings. If your tenant uses a default-deny allowlist, add Pleo's MCP server URL there as well.
Once approved, a Copilot Studio maker can add Pleo to an agent. See [How to Install the Pleo MCP in Microsoft Copilot](/docs/current/pleo-mcp/how-tos/installation/copilot).
***
## What Comes Next?
* [How to Install the Pleo MCP in Microsoft Copilot](/docs/current/pleo-mcp/how-tos/installation/copilot)
***
## Related Reading
* **Microsoft's documentation:** [Manage tools for agents in Microsoft 365 admin center](https://learn.microsoft.com/microsoft-365/admin/manage/manage-tools-for-agent)
* [FAQs](/docs/current/pleo-mcp/get-started/faq)
* [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions)
***
# How to Add Pleo MCP as a Custom Connector in Gemini Enterprise
Source: https://developers.pleo.io/docs/current/pleo-mcp/how-tos/enablement/gemini-enterprise
How a Gemini Enterprise admin registers Pleo as a Custom MCP Server so it's available to their organisation.
This page is for admins of **Gemini Enterprise** (Google Cloud), with the **Gemini Enterprise Admin** role or `roles/discoveryengine.editor`. Unlike Claude or ChatGPT, individual users can't add Pleo themselves: an admin must register it first.
## Registering Pleo as a Custom MCP Server
Gemini Enterprise's Custom MCP Server connector requires manually configured OAuth endpoint details rather than automatic client registration. [Contact Pleo](https://help.pleo.io/en/support/home) if you want to use the Pleo MCP in Gemini Enterprise.
See **Google's documentation:** [Set up your custom MCP server data store](https://docs.cloud.google.com/gemini/enterprise/docs/connectors/custom-mcp-server/set-up-custom-mcp-server)
***
## Related Reading
* **Google's documentation:** [Set up your custom MCP server data store](https://docs.cloud.google.com/gemini/enterprise/docs/connectors/custom-mcp-server/set-up-custom-mcp-server)
* [FAQs](/docs/current/pleo-mcp/get-started/faq)
* [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions)
***
# Pleo MCP Enablement Overview
Source: https://developers.pleo.io/docs/current/pleo-mcp/how-tos/enablement/overview
Choose your AI platform to enable the Pleo MCP for your organisation, and check whether any admin action is needed at all.
Enablement is the **organisation-level** step that allows members to add the Pleo MCP in their AI platform.
Whether you need it depends on your plan: individual plans (such as Claude Pro or ChatGPT Plus) usually need no admin action, while managed workspaces and tenants often restrict custom connectors until an admin allows them.
## Prerequisites
Enablement in your AI platform is separate from enablement in Pleo. A company admin must also enable MCP access in Pleo before anyone can connect. See [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions).
## Find Your Enablement Guide
These guides walk an **admin** through enabling the Pleo MCP at the **organisation level**, so individual members can then connect it themselves.
Select your AI platform below for the relevant enablement steps.
Add Pleo as an organisation-wide custom connector in Claude for Work.
Where org-level control actually lives for Codex, which has no admin console of its own.
Enable custom connectors for a ChatGPT Business, Enterprise, or Edu workspace.
Register Pleo as a Custom MCP Server in Google Cloud.
Approve Pleo as an allowed MCP server for Copilot Studio agents.
***
## What Comes Next?
* [Pleo MCP Installation Overview](/docs/current/pleo-mcp/how-tos/installation/overview)
***
## Related Reading
* [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions)
* [FAQs](/docs/current/pleo-mcp/get-started/faq)
***
# How to Install the Pleo MCP in ChatGPT
Source: https://developers.pleo.io/docs/current/pleo-mcp/how-tos/installation/chatgpt
Step-by-step instructions for connecting the Pleo MCP to ChatGPT Plus or Pro as a custom connector.
This page is for ChatGPT **Plus** and **Pro** users installing the Pleo MCP as a custom connector.
If you use a ChatGPT Business, Enterprise, or Edu workspace, a workspace admin must publish and install the Pleo MCP before members can authorise it. See [How to Install the Pleo MCP in ChatGPT Business](/docs/current/pleo-mcp/how-tos/installation/chatgpt-business).
## Prerequisites
Before you begin:
* Pleo MCP access is off by default; a company admin must enable it for your organisation before you can connect. See [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions) if you're not sure whether it's enabled.
## Steps
Follow the steps below to install and authorise the Pleo MCP in ChatGPT.
1. Go to **Settings → Apps & Connectors**.
2. Scroll to **Advanced settings** and turn on **Developer mode**. This unlocks the option to create custom connectors.
3. Back in **Apps & Connectors**, click **Create**.
4. Enter a name (for example, "Pleo MCP"), an optional description, and the connector URL:
```
https://mcp.pleo.io/mcp
```
5. Set **Authentication** to **OAuth**, then click **Create**.
6. Click **Connect**, log in to Pleo if needed, and approve access.
7. In any chat, click **+ → Connectors** and enable the Pleo MCP to use it in that conversation.
The Pleo MCP is now connected and ready to use in ChatGPT.
***
## Related Reading
* **OpenAI's documentation:** [Developer mode and MCP apps in ChatGPT](https://help.openai.com/en/articles/12584461)
* [Install the Pleo MCP: Choose Your AI Tool](/docs/current/pleo-mcp/how-tos/installation/overview)
* [How to Install the Pleo MCP in ChatGPT Business](/docs/current/pleo-mcp/how-tos/installation/chatgpt-business)
* [How to Add Pleo MCP as a Custom Connector in ChatGPT](/docs/current/pleo-mcp/how-tos/enablement/chatgpt)
* [How to Revoke Access to the Pleo MCP](/docs/current/pleo-mcp/how-tos/uninstallation/revoke-access)
***
# How to Install the Pleo MCP in ChatGPT Business
Source: https://developers.pleo.io/docs/current/pleo-mcp/how-tos/installation/chatgpt-business
Step-by-step instructions for publishing the Pleo MCP as a workspace app in ChatGPT Business, Enterprise, and Edu, and authorising it as a user.
This page is for ChatGPT **Business**, **Enterprise**, and **Edu** workspaces, where the Pleo MCP is published once by an admin and then authorised by each member.
If you're on ChatGPT Plus or Pro, you install the Pleo MCP yourself and there is no admin step. See [How to Install the Pleo MCP in ChatGPT](/docs/current/pleo-mcp/how-tos/installation/chatgpt).
## Prerequisites
Before you begin:
* Pleo MCP access is off by default; a company admin must enable it for your organisation before anyone can connect. See [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions) if you're not sure whether it's enabled.
* You need a workspace **Owner** or **Admin** role in ChatGPT to complete Part 1. Each member completes Part 2 for themselves.
* Custom MCP connectors must be permitted for your workspace. See [How to Add Pleo MCP as a Custom Connector in ChatGPT](/docs/current/pleo-mcp/how-tos/enablement/chatgpt).
Publishing the Pleo MCP to your workspace makes it available to everyone. It doesn't authorise the Pleo MCP for members or grant access to data they can't already see in Pleo. Each member must sign in and approve access, and the Pleo MCP only acts within that member's existing Pleo permissions.
## Part 1: Publish the Pleo MCP to the Workspace
These steps must be completed by a workspace Owner or Admin.
1. In ChatGPT, open the profile menu and go to **Workspace settings → Apps**, then click **Create**.
2. In the **New App** dialog, enter:
* **Name**: for example, "Pleo MCP". The name must be unique in the workspace. If you see *Connector name already exists*, choose another name.
* **Description** (optional): for example, "Manage your company expenses".
* **Connection**: leave **Server URL** selected and enter the Pleo MCP server URL: `https://mcp.pleo.io/mcp`
* **Authentication**: select **OAuth**. Leave **Advanced OAuth settings** unchanged; the Pleo MCP's OAuth configuration is discovered from the server URL.
3. Select **I understand and want to continue** under the custom MCP risk notice, then click **Create**.
**Create** produces a draft, not a live app. It can take a few seconds to appear. Open **Apps → Drafts** and refresh if you don't see it straight away.
4. Open the draft from **Apps → Drafts** and click **Enable**.
5. In the **Publish** dialog, expand and review both risk sections: **Unauthorized data access** and **Malicious app**. Select **I trust this app** and **I have reviewed the parameters being sent to each action**, then click **Publish**.
6. Open the Pleo MCP from the **Apps** list and configure its defaults:
* **Which actions the Pleo MCP can use**: choose what happens when the Pleo MCP adds actions in the future: **Enable all new actions**, **Only enable new read actions**, or **Disable new actions**.
* **When the Pleo MCP asks for approval**: **Use workspace default** is the recommended starting point. The alternatives are **Always ask**, **Allow read actions**, **Allow low-risk actions**, and **Allow all actions**. Only choose **Allow all actions** if you accept that ChatGPT can read and write in Pleo without prompting.
7. Go to **Workspace settings → Plugins**, open the Pleo MCP, and under **Availability**, change **Available** to **Installed**. You should see *Workspace availability updated*.
Don't skip the install step. A published app is only **Available**: members can find it, but it isn't added to their workspace. Changing its availability to **Installed** makes the Pleo MCP available to everyone.
The Pleo MCP is now published and installed for your workspace.
## Part 2: Authorise the Pleo MCP
Each member must authorise the Pleo MCP once. Publishing the app doesn't authorise it for them.
1. Go to **Settings → Plugins**. The Pleo MCP appears in the list after an admin installs it.
2. Click **Connect**, then **Continue to Pleo** in the dialog explaining how ChatGPT uses connector data.
3. Log in to Pleo if needed, then review the consent screen. It explains what the Pleo MCP allows ChatGPT to do. The Pleo MCP can't make payments and can't act outside your existing Pleo permissions. See [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions) for the full security boundaries.
4. Click **Connect**. You're redirected to ChatGPT, where the Pleo MCP now shows as connected.
5. Try it in a chat, for example: *"Fetch my Pleo expenses."*
See the **Troubleshooting** section below for quick steps to resolve the most common issues.
The Pleo MCP is now connected and ready to use in ChatGPT.
## Troubleshooting
Use this quick reference for steps to resolve the most common issues.
| What you see | What to do |
| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| *Connector name already exists* when creating the app | Choose a different name. App names must be unique within the workspace. |
| The draft doesn't appear under **Apps → Drafts** | Wait a few seconds and refresh. Creating a custom app isn't instant. |
| Members can't find the Pleo MCP in **Settings → Plugins** | Check that **Workspace settings → Plugins → Pleo MCP → Availability** is set to **Installed**, not only **Available**. |
| The Pleo MCP consent screen doesn't load or the redirect stalls | Start the connection again from **Settings → Plugins → Connect**. |
| A chat answers without using the Pleo MCP | Enable the Pleo MCP for that conversation through **+ → Connect plugins**, then repeat the prompt. |
For additional context, see the [FAQ](/docs/current/pleo-mcp/get-started/faq#troubleshooting).
***
## Related Reading
* **OpenAI's documentation:** [Admin Controls, Security, and Compliance in apps (Enterprise, Edu, and Business)](https://help.openai.com/en/articles/11509118)
* **OpenAI's documentation:** [Developer mode and MCP apps in ChatGPT](https://help.openai.com/en/articles/12584461)
* [Install the Pleo MCP: Choose Your AI Tool](/docs/current/pleo-mcp/how-tos/installation/overview)
* [How to Install the Pleo MCP in ChatGPT](/docs/current/pleo-mcp/how-tos/installation/chatgpt)
* [How to Add Pleo MCP as a Custom Connector in ChatGPT](/docs/current/pleo-mcp/how-tos/enablement/chatgpt)
* [How to Revoke Access to the Pleo MCP](/docs/current/pleo-mcp/how-tos/uninstallation/revoke-access)
***
# How to Install the Pleo MCP in Claude
Source: https://developers.pleo.io/docs/current/pleo-mcp/how-tos/installation/claude
Step-by-step instructions for adding Pleo as a custom connector in Claude Web, Claude Desktop, or Cowork.
This page is for Pleo users connecting Claude to Pleo. The same steps apply to **Claude Web**, **Claude Desktop**, and **Cowork**.
Searching for Pleo in Claude's connector directory won't find it. You add Pleo yourself as a custom connector, using the Pleo MCP server URL below.
## Prerequisites
Before you begin:
* Pleo MCP access is off by default; a company admin must enable it for your organisation before you can connect. See [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions) if you're not sure whether it's enabled.
* If your organisation manages Claude through Claude for Work, your Claude admin may have already added Pleo as an organisation-wide connector, or may restrict who can add their own. See [How to Add Pleo MCP as a Custom Connector in Claude](/docs/current/pleo-mcp/how-tos/enablement/claude).
## Steps
Follow the steps below to connect Pleo in Claude.
1. Go to **Customize** in the left-hand menu, then open **Connectors**.
2. If your Claude admin has already added Pleo for your organisation, find the **Pleo** connector in the list and skip to step 5.
3. Click **Add custom connector**, then enter a name (for example, "Pleo") and the Pleo MCP server URL:
```
https://mcp.pleo.io/mcp
```
| Property | Value |
| ------------------ | --------------------------------------- |
| **MCP URL** | `https://mcp.pleo.io/mcp` |
| **Transport** | Streamable HTTP |
| **Authentication** | OAuth 2.0 (no credentials to configure) |
No Advanced or Auth settings are needed.
4. Add the connector.
5. Click **Connect** for the Pleo connector.
6. Log in to Pleo if needed, then click **Approve**.
You are now connected and can start using Pleo in Claude.
Claude lets you set permissions per tool. Pleo recommends requiring approval for any tool that writes data, so you see each change before it's made in Pleo.
***
## What Comes Next?
* [Example Use Cases](/docs/current/pleo-mcp/get-started/use-cases)
***
## Related Reading
* **Anthropic's documentation:** [Get started with custom connectors using remote MCP](https://support.claude.com/en/articles/11175166)
* [Install the Pleo MCP: Choose Your AI Tool](/docs/current/pleo-mcp/how-tos/installation/overview)
* [How to Add Pleo MCP as a Custom Connector in Claude](/docs/current/pleo-mcp/how-tos/enablement/claude)
* [How to Revoke Access to the Pleo MCP](/docs/current/pleo-mcp/how-tos/uninstallation/revoke-access)
***
# How to Install the Pleo MCP in Codex
Source: https://developers.pleo.io/docs/current/pleo-mcp/how-tos/installation/codex
Step-by-step instructions for connecting the Pleo MCP to OpenAI's Codex.
This page is for developers connecting Codex CLI to Pleo by editing their local configuration file.
## Prerequisites
Before you begin:
* Pleo MCP access is off by default; a company admin must enable it for your organisation before you can connect. See [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions) if you're not sure whether it's enabled.
* If you use Codex through a ChatGPT Business or Enterprise workspace that restricts custom connectors, your workspace admin has allowed Pleo. See [How to Add Pleo MCP as a Custom Connector in ChatGPT](/docs/current/pleo-mcp/how-tos/enablement/chatgpt) if you're not sure.
## Steps
Follow the steps below to connect Pleo to Codex. Codex CLI reads its MCP server configuration from `~/.codex/config.toml`.
1. Add a Pleo entry to `~/.codex/config.toml`, either by editing the file directly or with the `codex mcp add` command:
```bash theme={null}
codex mcp add pleo-mcp --url https://mcp.pleo.io/mcp
```
This adds a block like the following to your config file:
```toml theme={null}
[mcp_servers.pleo-mcp]
url = "https://mcp.pleo.io/mcp"
```
2. Authorise the connection:
```bash theme={null}
codex mcp login pleo-mcp
```
This opens your browser for OAuth authorisation. Log in to Pleo if needed, then approve access.
3. Run `codex mcp list` to confirm `pleo-mcp` is configured and connected.
You are now connected and can start using the Pleo MCP in Codex.
Unlike some MCP clients, Codex doesn't prompt for OAuth automatically the first time a tool is called. You must run `codex mcp login ` yourself after adding the server. Use `codex mcp logout pleo-mcp` to remove stored credentials if you need to reconnect.
***
## Related Reading
* **OpenAI's documentation:** [Model Context Protocol – Codex](https://developers.openai.com/codex/mcp)
* [Install the Pleo MCP: Choose Your AI Tool](/docs/current/pleo-mcp/how-tos/installation/overview)
* [How to Add Pleo MCP as a Custom Connector in Codex](/docs/current/pleo-mcp/how-tos/enablement/codex)
* [How to Revoke Access to the Pleo MCP](/docs/current/pleo-mcp/how-tos/uninstallation/revoke-access)
***
# How to Install the Pleo MCP in Microsoft Copilot
Source: https://developers.pleo.io/docs/current/pleo-mcp/how-tos/installation/copilot
Step-by-step instructions for connecting the Pleo MCP to a Copilot Studio agent.
This page is for Copilot Studio makers adding Pleo as an MCP tool to an agent, not every individual Copilot user. If you use a Copilot agent that a maker has already connected to Pleo, you don't need to do anything here beyond the first-time Pleo sign-in prompt.
## Prerequisites
Before you begin:
* Pleo MCP access is off by default; a company admin must enable it for your organisation before you can connect. See [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions) if you're not sure whether it's enabled.
* If your Microsoft 365 tenant blocks custom MCP servers by default, your tenant admin has approved Pleo. See [How to Add Pleo MCP as a Custom Connector in Microsoft Copilot](/docs/current/pleo-mcp/how-tos/enablement/copilot) if Pleo doesn't connect.
## Steps
Follow the steps below to connect Pleo to a Copilot Studio agent.
1. Open your agent in **Copilot Studio** and go to **Tools → Add a tool → New tool → Model Context Protocol**.
2. Enter a server name (for example, "Pleo") and the server URL:
```
https://mcp.pleo.io/mcp
```
3. Under **Authentication**, choose **OAuth 2.0** and select **Dynamic discovery**. If your agent doesn't support dynamic discovery, choose **Manual** and contact Pleo for the OAuth endpoint details (Authorization URL, Token URL, Client ID, and Client Secret).
4. Click **Create a new connection**, then **Add to agent**.
5. Log in to Pleo if needed, then approve access.
6. In the agent's tool list, toggle on the specific Pleo tools you want the agent to use.
Your agent must have generative orchestration enabled for it to decide when to call Pleo's tools.
Pleo is now available as a tool for this agent.
See **Microsoft's documentation:** [Connect your agent to an existing MCP server](https://learn.microsoft.com/microsoft-copilot-studio/mcp-add-existing-server-to-agent)
***
## Related Reading
* [Install the Pleo MCP: Choose Your AI Tool](/docs/current/pleo-mcp/how-tos/installation/overview)
* [How to Add Pleo MCP as a Custom Connector in Microsoft Copilot](/docs/current/pleo-mcp/how-tos/enablement/copilot)
* [How to Revoke Access to the Pleo MCP](/docs/current/pleo-mcp/how-tos/uninstallation/revoke-access)
***
# How to Install the Pleo MCP as a Custom MCP
Source: https://developers.pleo.io/docs/current/pleo-mcp/how-tos/installation/custom-mcp
Connect any MCP-compatible client to the Pleo MCP server, including staging setup.
This page covers connecting a client to the Pleo MCP server. Choose the path that matches your client.
## Prerequisites
Before you begin:
* Pleo MCP access is off by default; a company admin must enable it for your organisation before you can connect. See [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions) if you're not sure whether it's enabled.
* You have access to a supported MCP client or terminal.
## Installing the Pleo MCP as a Custom MCP
The Pleo MCP server is a standard remote HTTP MCP endpoint. Any client that supports remote MCP connections can connect using the production URL:
```
https://mcp.pleo.io/mcp
```
| Property | Value |
| ------------------ | ------------------------------------------------------ |
| **Production URL** | `https://mcp.pleo.io/mcp` |
| **Staging URL** | `https://mcp.staging.pleo.io/mcp` |
| **Transport** | Streamable HTTP |
| **Authentication** | OAuth 2.0 (browser-based, no credentials to configure) |
## Using Staging
Pleo provides a staging MCP server for testing against non-production data.
### Steps
Follow the steps below to connect to the staging server.
1. In the staging Pleo app, go to **Settings → General → Pleo AI**. Under the **MCP** section, enable **Pleo MCP access** for your entity.
2. Use the staging URL in place of the production URL:
```
https://mcp.staging.pleo.io/mcp
```
For Claude Code:
```bash theme={null}
claude mcp add --transport http pleo-mcp-staging https://mcp.staging.pleo.io/mcp
```
You are now connected to the staging MCP server.
Staging and production MCP connections use separate OAuth sessions and separate server names. You will be prompted to authorise each independently.
***
## Related Reading
* [Install the Pleo MCP: Choose Your AI Tool](/docs/current/pleo-mcp/how-tos/installation/overview)
* [Pleo MCP Capabilities](/docs/current/pleo-mcp/get-started/pleo-mcp-capabilities)
* [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions)
***
# Pleo MCP Installation Overview
Source: https://developers.pleo.io/docs/current/pleo-mcp/how-tos/installation/overview
Choose your AI tool to connect the Pleo MCP, or find the custom MCP setup guide if your tool isn't listed.
## Prerequisites
MCP access is off by default. A company admin must enable it in Pleo before you can connect. See [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions) if you're not sure whether MCP is enabled for your organisation.
## Choose Your AI Tool
Pick the guide that matches the AI assistant where you want to install the Pleo MCP.
Add Pleo as a custom connector in Claude Web, Claude Desktop, or Cowork.
Connect Pleo to OpenAI's Codex.
Connect Pleo using ChatGPT's Connectors settings.
Install the Pleo MCP in a Business, Enterprise, or Edu workspace, then let each member authorise it.
Install the Pleo MCP in Microsoft 365 Copilot.
Don't see your AI tool listed? It may still work with the Pleo MCP by installing it as a custom MCP.
Install Pleo MCP in any tool supporting custom MCP.
***
## What Comes Next?
* [Example Use Cases](/docs/current/pleo-mcp/get-started/use-cases)
***
## Related Reading
* [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions)
* [How to Revoke Access to the Pleo MCP](/docs/current/pleo-mcp/how-tos/uninstallation/revoke-access)
* [FAQs](/docs/current/pleo-mcp/get-started/faq)
***
# How to Revoke Access to the Pleo MCP
Source: https://developers.pleo.io/docs/current/pleo-mcp/how-tos/uninstallation/revoke-access
How to revoke Pleo MCP access from inside Pleo, or disconnect it from your AI client.
This page is for users revoking their own Pleo MCP access, either from inside Pleo or from their AI client.
## Revoke Access from Inside Pleo
Go to **My Account → Security and Devices → MCP Connections** and revoke the connection.
## Disconnect from Your AI Client
You can also remove the connection from inside your AI client. The exact steps vary by tool. In Claude, for example:
Go to **Connectors**, find the Pleo Connector, and click **Disconnect**.
For other tools, check that tool's connector or app settings and remove Pleo from there.
***
## Related Reading
* [Install the Pleo MCP: Choose Your AI Tool](/docs/current/pleo-mcp/how-tos/installation/overview)
* [Access & Permissions](/docs/current/pleo-mcp/get-started/access-and-permissions)
***
# Expense Types
Source: https://developers.pleo.io/docs/expense-types
This page lists all expense types and subtypes in Pleo, as returned by `GET /v3/export-items` in `data[].type` and `data[].subType`.
## Card Expenses
**Enum:** `card_purchase`
An expense made using Pleo cards.
**Example:** Sales employees of your company have Pleo cards. They travel to a customer from Amsterdam to Rotterdam by train and pay for the train ride with their Pleo cards.
**Enum:** `card_invoice`
An invoice paid using a card, containing details of an invoice number, amounts, supplier information, and taxes. Especially relevant in Spain.
**Enum:** `fee`
Any fees charged to the customer, including forex fees, overdraft fees, and interest.
The following `data[].subType` values may apply to `data[].type = fee`.
| Subtype | Enum | Description |
| -------- | ---------- | --------------------------------------------------------- |
| Interest | `interest` | Interest charged as a fee, typically for overdraft usage. |
## Invoices
**Enum:** `invoice`
Invoice document with details of invoice number, amounts, supplier information, and taxes. Appears in the Pleo Export page only after it has been paid.
**Example:** Your company has received an invoice from its IT service provider for €1,500. Your accountant uploads this invoice to Pleo to make payment at a later date.
**Enum:** `invoice_payment`
Information about the payment made for an invoice.
**Example:** Your company has paid €1,500 to the IT service provider.
The following `data[].subType` values may apply to `data[].type = invoice_payment`.
| Subtype | Enum | Description |
| ------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Invoice Payment Sent | `invoice_payment_sent` | Invoice payment that was successfully sent, confirming the payment for an invoice. |
| Invoice Payment Returned | `invoice_payment_returned` | Invoice payment that was returned or reversed, typically due to payment failure or cancellation after the payment was initially processed. |
## Pocket Expenses
**Enum:** `out_of_pocket`
An expense made by an employee using their personal funds, for which they need to be reimbursed.
**Example:** An employee buys office supplies worth €30 during a business trip at a local store that does not accept company cards. The employee submits the expense with a receipt through Pleo for reimbursement.
**Enum:** `mileage`
An allowance which a company can pay out to employees for driving their own car on business trips.
**Example:** Your employee drives 100 kilometres to attend a business conference. Using Pleo's mileage tracking feature, they record the trip to get reimbursed at the company's rate of €0.40 per kilometre, totalling €40.
**Enum:** `per_diem`
A daily travel allowance for business travel.
**Example:** Your employee is sent on a three-day business trip. Your company provides a per diem of €50 per day to cover meals and incidental expenses. Over three days, they receive a total of €150.
## Reimbursements
**Enum:** `direct_reimbursement`
Refund, by directly transferring funds from the Pleo Wallet, to an employee's personal card or bank account for an expense made using their personal funds.
**Example:** An employee paid for a client's dinner from her own pocket and now gets this money back to her personal bank card. The money was taken from the Pleo Wallet.
The following `data[].subType` values may apply to `data[].type = direct_reimbursement`.
| Subtype | Enum | Description |
| ------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Reimbursement to Employee | `reimbursement_to_employee` | Reimbursement paid to an employee for expenses made out of pocket, either through direct transfer or external payment method. |
**Enum:** `external_reimbursement`
Refund, through cash or payroll, to an employee for an expense made using their personal funds. This is handled outside of Pleo and does not affect the Pleo Wallet balance.
The following `data[].subType` values may apply to `data[].type = external_reimbursement`.
| Subtype | Enum | Description |
| ------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Reimbursement to Employee | `reimbursement_to_employee` | Reimbursement paid to an employee for expenses made out of pocket, either through direct transfer or external payment method. |
| Reimbursement to Company | `reimbursement_to_company` | Reimbursement paid to the company, typically when an employee returns funds or makes an accidental purchase with a Pleo card. |
## Returns
**Enum:** `refund`
Refund on a spend from the supplier.
**Example:** You paid for a hotel booking with your Pleo card. Your plans changed and you cancelled the booking. You received a refund from the hotel.
The following `data[].subType` values may apply to `data[].type = refund`.
| Subtype | Enum | Description |
| -------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Reimbursement to Employee Refund | `reimbursement_to_employee_refund` | Return of a reimbursement to an employee that bounced and/or failed, reversing the original reimbursement transaction. |
**Enum:** `chargeback`
Return of funds for a previous card expense, usually as a result of a dispute.
**Example:** Your company is billed twice by a conference organiser for a single event, with two charges of €300 each. The Pleo Admin disputes the second charge through the card issuer's chargeback process and the €300 is reimbursed to your company's Pleo account.
## Wallet Transfers
**Enum:** `wallet_topup`
Transferring funds to the Pleo Wallet from your company's bank account.
**Enum:** `wallet_unload`
Withdrawing funds from the Pleo Wallet to your company's bank account.
## Other
**Enum:** `atm_withdrawal`
Cash withdrawn from an ATM using a Pleo card. Since Pleo cannot control how an employee uses the withdrawn money, ATM withdrawals are considered as an employee taking company money for personal use. This has implications for how ATM withdrawals are typically booked.
**Example:** An employee is on a business trip to a region where cash payments are more common. They use their Pleo card to withdraw €200 from an ATM to cover meals and local transportation.
**Enum:** `personal_spend`
An expense made by an employee using their Pleo card for personal reasons.
**Example:** Employees of your company use their Pleo cards to pay for personal grocery bills amounting to €50. They mark the transaction as personal spend in the Pleo app.
**Enum:** `cashback`
Cashback offered by Pleo, a way to reward customers for spending on Pleo.
## Related Reading
* [How to Fetch Export Item Data for Processing](/docs/current/how-tos/accounting-integrations/how-to-fetch-export-item-data-for-as-erp-processing)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Integration: Quickstart](/docs/current/getting-started/accounting-integrations-quickstart)
# Standalone API Key Generation
Source: https://developers.pleo.io/docs/for-partners
* [Standalone API Keys Overview](/docs/current/authentication/standalone-api-keys-overview)
* [Standalone API Key Workflow Guide](/docs/current/guides/standalone-api-keys-workflow-guide)
* [How to Make an API Call Using a Standalone API Key](/docs/current/how-tos/api-keys/how-to-make-an-api-call-using-standalone-api-keys)
You can generate standalone API keys when you want to integrate your Pleo account with custom applications (applications that are not listed as part of available integrations).
* You must already have access to the **Pleo Web App** in **Staging** or **Production** (you cannot sign in with a personal email account and generate an API key).
* You need **Admin** or **Bookkeeper** permissions to see the option to generate API keys.
* If you have the required permissions but do not see the **API Keys** option under **Settings**, API Key access is not available. It is determined during onboarding.
1. In the Pleo application, go to **Settings**.
2. Click **API Keys**. The **API Keys** page appears.
3. Click **Create API Key**.
4. Enter the following details:
* **Name**: An appropriate name for the API key you are generating.
* **Expiration**: Specify a date until when you want the API key to be active.
* **Access Level**: Specify whether you want the API key for a specific entity (company) or for multiple entities of a business (organisation).
* **Permissions**: Select the check boxes next to the scopes you require. Based on your selection, the scopes are enabled in the API key generated at the end of this procedure.
The API key is generated. Copy the API key for future reference.
On the **API Keys** page, you can view the list of API keys that you have generated. To remove an API key, click the specific row and click **Delete**.
# Generic Terms in Pleo
Source: https://developers.pleo.io/docs/generic-terms-in-pleo
Common terms used frequently across Pleo's documentation and APIs.
Represents an account of a company or organisation with Pleo, containing a specific fund in a specific currency. Customers use Pleo accounts for invoice payments, vendor payments, and reimbursements. Funds can be added, converted, or withdrawn. Pleo supports multi-currency configuration — customers can hold accounts in up to six different currencies.
Payments made with the Pleo card, using funds from the Pleo wallet. This is at the company or organisation level.
Accounts in the ERP or accounting system that correspond to Pleo accounts. Out of Pocket expenses and Reimbursements indicate the amount in a Pleo account that a company or organisation owes to its employees.
Cash withdrawn by employees using their Pleo card. This is recorded as an employee's liability to the company.
The chart of accounts is the customer's index of all financial accounts in an ERP or accounting system. An account in the chart of accounts is the source or destination of money — it could be a real physical entity such as a bank account, or a virtual account such as a pocket account.
## Related Reading
* [Accounts Mapping](/docs/current/platform/exports/accounts-mapping)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [AS/ERP Integration: Quickstart](/docs/current/getting-started/accounting-integrations-quickstart)
# Implementing the OAuth 2.0 Client Using a Library
Source: https://developers.pleo.io/docs/implementing-oauth-client-using-a-library
* **[OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)**
Many open source libraries offer high-quality implementations of OAuth 2.0 clients. They are available in a variety of programming languages, offering support for various web frameworks and database backends.
Where to start. A list of open-source OAuth 2.0 client libraries is available at [https://oauth.net/code/](https://oauth.net/code/).
We strongly recommend using one of these libraries instead of implementing OAuth 2.0 from scratch. Examples are:
* **Java**: spring-security-oauth2-client, Apache OLTu
* **Python**: authlib, Requests-OAuthlib
* **JavaScript**: passport.js, oidc-client-js
* **.NET**: Microsoft.Identity.Web
## Implementing OAuth 2.0 Client Using a Library
Libraries providing OAuth 2.0 implementation often allow customisation to some extent - allow to plug in custom implementations for front-end and back-end interfaces they provide.
Most often, when using a library, a developer can:
Benefits of using a library:
* Configure the client with specific authorisation server: provide the authorisation and token endpoints, configure supported client authentication methods, and protocol extensions.
* Provide the User Interface (UI) for initiating the authentication flow (**connect to Pleo** button), and connect it to functions and methods of the library that generate all necessary authentication request parameters.
* Integrate the library with the web framework of choice to allow it to perform HTTP redirects.
* Provide implementation for persistence backend, allowing the client library to store and retrieve parameters, such as `code_verifier` and `state`. Also, generate access and refresh tokens.
* Provide custom UI for handling success and error responses from the authorisation server.
* Listen to events emitted during flow and extend various stages.
Details and capabilities of OAuth 2.0 implementations depend of the library
your are selecting. Consult the documentation of the specific library you
have selected.
## Related Topics
* [OAuth 2.0 Implementation Reference Documentation](/docs/oauth-implementation-reference-documentation): Refer to this document to understand how the OAuth 2.0 authentication method is implemented.
# Integrated API Key Generation
Source: https://developers.pleo.io/docs/integrated-api-key-generation
* [Integrated API Keys Overview](/docs/current/authentication/integrated-api-keys-overview)
You can generate integrated API keys while you are connecting your Pleo account with an application that is already existing in the list of Pleo supported integrations.
As a customer, you are generating an integrated API key during the integration installation process; this is the crucial difference between integrated API key and standalone key. The standalone API key is not relevant to a specific integration.
## Characteristics of Integrated API Keys:
* The API key is generated during the integration process and hence, it includes the permissions required to successfully authenticate the integration.
* Although the integrated API keys are closely connected to the integrations for which they are generated, yet they can be used, in general, to manage your Pleo account in order to protect the confidentiality and security of your business.
* You can see them as part of the list of API keys generated in the past on the **Settings > API Keys** page.
## How to Generate an Integrated API Key?
**Microsoft Dynamics 365 Finance** is selected here as an example.
1. In the Pleo application, go to **Settings**.
2. Click **Accounting**.
3. Click **Microsoft Dynamics 365 Finance** as the application that you want to integrate with your Pleo account.
4. Click **Allow**.
The integrated API key is generated. Copy the API key for future reference.
# Inviting Users to Pleo and Assigning User Roles
Source: https://developers.pleo.io/docs/inviting-users-to-pleo-and-assigning-user-roles
* **[Employees and Roles](/docs/current/platform/employees/employees-and-roles)**
A company or an organisation must add their employees and bookkeepers to Pleo.
## User Roles in Pleo
In an organisation, Pleo caters to different user roles, including the following:
| User Role | Supported or Not | Privileges |
| :----------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Employees | ✅ | • Use Virtual and physical Pleo cards to manage business expenditures. • Submit out-of-pocket expenses for reimbursements. |
| Administrators | ✅ | • Handle all company/organisation expenses • Manage Pleo wallet • Handle subscription expenses • Order and block cards for employees of your company/organisation • Manage spending limits of employees of your company/organisation • Manage ERP/accounting integrations • Export accounting entries to ERP/accounting system • Payment of invoices • Analyse accounting data of your company/organisation |
| Expense reviewers | ✅ | • Review expenses submitted by the team or project (up to a certain limit) • Specify and control spending limits • Block and unblock Pleo cards of employees |
| Bookkeepers | ✅ | External bookkeepers can either have limited or extended permissions in your Pleo account.
**Limited access** includes: • View and modify expense details • View Invoices (not authorised to pay invoices) • Modify accounting settings • Export accounting entries to ERP/accounting system • Reimburse out-of-pocket expenses (enabled by default, can be disabled) • Move funds through the Pleo wallet (disabled by default, can be enabled)
**Extended access** includes: • Add employees to Pleo and modify user roles - access to the **People** page • View Invoices (not authorised to pay invoices), except in the United Kingdom • Specify and modify spending limits • Wallet top-up (not applicable to the United Kingdom) • Order cards • Assign permission of expense reviewing to employees • Reimburse out-of-pocket expenses |
| Finance department | ✅ | |
## Adding Employees to Pleo
You can add your employees to Pleo and assign different permissions based on the activity they would perform in Pleo. There are two ways to add your employees:
* Add employees in Pleo.
* Import employees from the Human Resources Information System (HRIS) that you have integrated with.
You can also add a Pleo specific **employee code** to map the employee details with the record existing in the HRIS or ERP system.
### Adding Employees in Pleo
Perform the following to add employees in Pleo:
1. In the Pleo application, click **People**.
2. On the **People** page, click **Add People**.
3. Enter the email address of the employee and click **Continue**.
4. Select the appropriate card access that you want to provide to this employee:
* **Card only**: Select this option if the employee needs a Pleo card for all business spending.
* **Card and Reimbursements**: Select this option if the employee needs a Pleo card for all business spending and might submit out-of-pocket expenses for reimbursements.
* **Reimbursements only**: Select this option if the employee would only submit out-of-pocket expenses for reimbursements.
### Assigning User Roles to Employees
If you have **Admin** rights, you can assign user permissions to employees, based on the responsibilities they would perform. For more information see [Assigning User Roles to Employees](https://help.pleo.io/en/support/solutions/articles/103000254557-how-to-change-user-roles).
## Adding External Bookkeepers to Pleo
You can add external bookkeepers to your Pleo account.
External bookkeepers do not have permissions to manage Pleo cards. If the bookkeeper is an internal employee of your company or organisation, assign the Admin role to the bookkeeper. An admin user has access to perform all tasks in the Pleo application, including bookkeeping responsibilities.
### Adding External Bookkeepers in Pleo
Perform the following to add external bookkeepers in Pleo:
1. In the Pleo application, click **People**.
2. On the **People** page, click **Add People**.
3. Click **Invite external bookkeeper**.
4. Select whether you want to provide your bookkeeper with limited or extended access.
5. In the **Invite Bookkeeper** pop-up, enter the name and the email address of the bookkeeper.
6. Click **Accept & Invite**.
You can invite external bookkeepers to your Pleo account if your company/organisation is on an **Enterprise** plan with Pleo.
# Learn more about Pleo
Source: https://developers.pleo.io/docs/learn-more-with-pleo
* **[What is Pleo](/docs/current/getting-started/what-is-pleo)**
* **[Platform Overview](/docs/current/getting-started/platform-overview)**
### What can I do with Pleo?
Pleo is an expense management solution that helps your organisation with all aspects of business spending. We help you to simplify expense reporting and automate financial workflows; thus saving your time and effort in managing and bookkeeping accounting entries.
A few benefits are highlighted here:
* **Employees purchasing directly with your company specific Pleo cards**:
This streamlines your business spending - you can set spending limits and every incurred expense is reviewed to verify that all details are appropriately included and conforms with company spending guidelines. In addition, the process of incurring expenses from personal wallet and then submitting the expenses for reimbursement is cumbersome as it makes the process lengthy and could lead to unnecessary delays.
With Pleo, you can eliminate any risk of overspending and also detect any missing expense information. Hence, you get accurate real-time accounting analytics.
* **A centralised platform to manage all types of expenses**:
In Pleo, you can manage all kinds of expenses, including reimbursements, allowances, subscription payments, and work-related travelling cost; for example, reimbursements for out of pocket expenses that your employees have incurred. Processing payments for any expense is now easier and faster as you spend less time and resources on reconciliation.
The only clause - the expenses are legitimate as per your company policy. The process is simple:
Expenses submitted.
Expenses reviewed and approved.
Payments processed.
* **Integrate Pleo with other Enterprise Resource Planning (ERP) systems/ Accounting software and Human Resources Information Systems (HRIS)**:
You can integrate Pleo with most ERP and accounting systems, providing you a seamless transition of accounting data from Pleo to your primary accounting software. This helps you to synchronise expenses across multiple systems and thus, empowering you to improve budget allocations, financial policy amendments, and revision of work policies.
Similarly, integration of Pleo with HRIS implies improved user management - provisioning Pleo cards for your new employees is an easy procedure, while deactivating the existing Pleo cards for your former users is also a simple task.
### How do customers use Pleo?
Here is a step by step procedure on how customers can leverage Pleo. For your understanding, the workflow is divided into two phases:
**First phase**:
1. **Onboarding**: Customers configure their Pleo account.
2. **Integrating with ERP/Accounting Systems**:
Integrate Pleo with the ERP/Accounting System.
Authenticate and authorise the integration.
3. **Pleo account configuration**: Configure the accounting data in Pleo to match with the third-party accounting software. For more information, see [Configuring Customer Account in Pleo](/docs/configuring-customer-account-in-pleo).
4. **Pleo wallet top up**: Add funds to the Pleo wallet. This helps customers to process all payments through their Pleo account.
5. **Addition of Employees**:
Add employees to their Pleo account.
Issue virtual and physical Pleo cards to their employees.
**Second Phase: Expense Review and Accounting Data Export**:
As employees spend with their Pleo cards, submit out-of-pocket expenses for reimbursement, or pay invoices, data is captured in Pleo.
1. Each expense moves through a workflow of data gathering and approvals.
2. Once the data collection is complete and an expense is approved, it is ready for export to the ERP/accounting system.
The exact steps of the workflow might be different for each organisation,
based on the specific business requirements.
3. The data is exported from Pleo into the accounting system. Based on the specific business needs, the data export to the ERP/accounting system could be scheduled at different intervals. Once an accounting entry is exported to the accounting software, it is **archived** in Pleo and is locked from further editing.
***
# Legacy Docs Overview
Source: https://developers.pleo.io/docs/legacy-note
This folder contains legacy versions of documentation that have since been rewritten or replaced.
These pages are provided for reference only and may not reflect current functionality or best practices.
For the most accurate and up-to-date guidance, refer to the main documentation.
Each legacy page includes links to the corresponding updated sections where available.
# Multi-Entity Setup with Pleo
Source: https://developers.pleo.io/docs/multi-entity-setup-with-pleo
* **[Multi-Entity and API Scope](/docs/current/platform/multi-entity/multi-entity-and-api-scope)**
This topic describes the difference between a company and an organisation and how Pleo supports businesses that have multiple subsidiaries.
## Difference between Company and Organisation
| Company | Organisation |
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| A single entity of a business, identified by a `company_id` maintained in Pleo.
For example, A company named Alexey Software has a single entity and operates from a single location, the United Kingdom. | Multiple entities of a business, identified by `organisation_id` maintained in Pleo.
For example, Love Creme Brulee is an organisation that has multiple subsidiaries and operates in different regions of Europe - Netherlands, Germany, and Belgium. The organisation Love Creme Brulee is assigned an `organisation_id` and each entity is assigned a `company_id`. An admin of Love Creme Brulee would have the privileges to view details of each entity. |
## How Pleo Supports Organisations?
We have implemented **multi-entity support** for organisations to optimally use Pleo as their spend management platform; a single Pleo account might have multiple entities with the following:
* Different accounting entries for each entity
* Different employees and vendors for each entity
* Different integrations for each entity
## Company and Organisation Specific Integrations
* **Third-party ERP/accounting** integrations with Pleo could be connected at the company level, implying `company_id` is used when connecting a Pleo account with a third-party ERP/accounting application.
* For **HRIS/user-management** integrations, the Pleo account could be connected both at organisation and company levels. If a business has only a single entity, HRIS integrations for this specific customer are done at the company level.
Each integration with Pleo can be authenticated either by API keys or OAuth 2.0. Hence, for accounting integrations, both API keys and OAuth 2.0 are generated at the company level; so, if an organisation has multi-entity set up, the authentication credentials are valid only for the entity for whom they are generated.
## Related Topic
For more information on this, refer [here](https://help.pleo.io/en/support/solutions/articles/103000254598-what-is-multi-entity-management-).
# Overview
Source: https://developers.pleo.io/docs/oauth-20
* **[OAuth 2.0 Overview](/docs/current/authentication/oauth/oauth-overview)**
* **[OAuth 2.0 Access to Staging Workflow Guide](/docs/current/guides/environment-access/staging-oauth-workflow)**
* **[OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)**
## What is OAuth 2.0?
**OAuth 2.0** is an industry standard for secured authorisation of web applications, allowing them to request access to user-owned resources from external resource providers, without asking users for their access credentials, such as passwords.
A few important characteristics of OAuth 2.0:
* In order to grant access, OAuth 2.0 does not request for users' credentials.
* OAuth 2.0 also restricts actions — it keeps a check on what an external website/application performs on the resources hosted on other websites/applications.
## Roles
OAuth 2.0 is a standard designed for access delegation. This includes the following four roles participating in the OAuth 2.0 protocol (explained here in the context of integrating with Pleo):
* **Resource owner**: Pleo's customer - a company or an organisation (multi-entity set up) that owns a number of protected resources, such as accounting data, list of employees, etc.
* **Resource server**: Pleo APIs provides access to the protected resources.
* **Client** : Third-party applications that the Developer Partner wants to integrate with Pleo.
* **Authorisation server**: Pleo provides an OAuth 2.0 implementation that fulfils the following:
* Allows clients to request access to resource servers.
* Helps resources owners to grant access to clients.
## Use of OAuth 2.0 in Pleo
In Pleo, we provide an implementation of an OAuth 2.0 authorisation server to help you with the following:
* Integrate third-part applications with Pleo using OAuth 2.0 as the authorisation method.
* Use OAuth 2.0 to access the Pleo APIs.
## How to use OAuth 2.0?
In order to integrate your application with Pleo using OAuth 2.0, you must perform the following:
1. [Register your application](/docs/oauth-client-registration) and receive OAuth 2.0 client credentials - client identifier and client secret.
2. [Integrate an OAuth 2.0 client library](/docs/implementing-oauth-client-using-a-library) in your application.
3. [Configure your OAuth 2.0 client](/docs/client-configuration) to work with Pleo's authorisation server.
After completing these steps, users of your application could successfully and securely transition data from Pleo.
# OAuth 2.0 Client Registration
Source: https://developers.pleo.io/docs/oauth-client-registration
* **[OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)**
To initiate the process of OAuth 2.0 authentication, you must first register a **Client**. To do so, provide registration data about your client in the form we provide. For your understanding, the registration data is categorised into the following types:
* Human-readable information
* Pleo specific information
* Technical information
## Registration Data
### Human-Readable Information
Human-readable information represents the following to the end-users:
* Branding of the client
* Link to the home page of the application or a page announcing the integration of the application with Pleo
* Legal Policy documentation
Do not use Pleo branding. In human-readable information, do not mention Pleo's name and branding. Also, instead of naming the application branding as a client of Pleo (**Example Client for Pleo**), just use **Example Client**.
The following table lists the human-readable parameters required for registering a client:
| Parameters | Descriptions |
| :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Client Name | A brand name of your application. |
| Client URI | A link to the home page of your application or a landing page announcing the integration of your application with Pleo. |
| Logo | An image representing the logo of your application. Square images (1:1 aspect ratio) in high resolution work best. |
| Terms of Service | Link to human-readable legal document describing contractual relationship between the end-user and the client, which is accepted by the end-user during authorisation of the client. |
| Privacy Policy | Link to human-readable legal document that describes how the deployment organisation collects, uses, retains, and discloses personal data of end-users. |
| Contacts | Contact information of people responsible for this client, typically email addresses. |
### Pleo Specific Information
The following table lists the parameters that are specific to integration of your client with Pleo:
| Parameters | Descriptions |
| :------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Initiation URI | A link to a page on which the end user can start the process of connecting your application with Pleo. This allows your client to be listed in the catalogue of applications integrated with Pleo. |
| Settings URI | A link to a page hosting the user interface to changing preferences of your application. Allows to link to the configuration page of your application directly from within Pleo product applications. |
### Technical Information
Technical information is used internally in OAuth 2.0 protocol to implement the redirection between the Pleo authentication server and the client.
| Parameters | Descriptions |
| :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Redirect URIs | A list of URIs implementing the OAuth 2.0 redirection endpoint. For more information, see [Redirect URIs](/docs/client-registration). |
| Scopes | A list of API scopes that a client is allowed to access in Pleo. This is provided to the client when Pleo generates the access tokens. |
| PKCE Support | By default, we require all client to implement PKCE extension of OAuth 2.0 protocol to secure it against certain types of attacks. However, if this requirement cannot be met for technical reasons, we allow exceptions from this requirement for certain clients on an individual basis. If your client doesn't support PKCE ([RFC 7636](https://www.rfc-editor.org/rfc/rfc7636)), you must request to mark it as `not supporting PKCE` when registering your client. |
| Subject Type Preference | (Optional, Pleo-specific) The default type of resources your application is capable of working with. |
## Client Credentials
After completing registration, you would receive a set of credentials for your client:
* **Client Identifier**: This is a unique identifier that makes your client easily identifiable from others. In addition, it allows Pleo to present the branding of your client to end-users when they are asked to authenticate their identity while trying to access the Pleo data. Client identifier is not a confidential information and is visible to anyone attempting to connect your client with Pleo using OAuth 2.0.
* **Client Secret**: Requests from the client are authenticated using the **Client Secret**, most notably, during access token request. As the name suggests, this information is confidential and you must store this securely.
* Client secret must never be stored on or exposed to a device to which the end user has direct access. For example, it cannot be embedded in the code of the application that runs on users’ devices or transferred over network to a user's device.
* Client secret may only be stored and used on a secured server component of your client from where it cannot be extracted easily.
These credentials are confidential and must be implemented in such a way that it is impossible for any end user of your client or any unauthorised party to extract the information.
The **Pleo OAuth 2.0 Authorisation Server** does not support any **public** client that cannot protect the confidentiality of the authentication credentials.
### Client Registration Example
A hypothetical client can be registered using the following parameters:
| Parameters | Values |
| :---------------------- | :----------------------------------------- |
| Client Name | "Example Client" |
| Client URI | `https://client.example/` |
| Terms of Service | `https://client.example/legal/tos.html` |
| Privacy Policy | `https://client.example/legal/privacy.pdf` |
| Contacts | `[email protected]` |
| Redirect URIs | `https://client.example/callback` |
| Scopes | `test:test users:read users:write` |
| PKCE Support | Supported |
| Subject Type Preference | None |
After registration, partners receives a set of client credentials.
| Credentials | Values |
| :---------------- | :------------------------------------- |
| Client identifier | `36e3b610-56d7-4d36-92c7-a003ca7bfc5f` |
| Client secret | `70771f3cbf472ba916aefd21be9c7a` |
These values are explained in the Example section of [Implementing OAuth 2.0 Reference Documentation](/docs/oauth-implementation-reference-documentation).
## Redirect URIs
Redirect URI indicates the location where the users are redirected after successful authentication. Hence, in the context of Pleo integration with your client, after the **Pleo Authorisation Server** authenticates your client, the users are redirected to the specific URI.
HTTP redirections are crucial in OAuth 2.0 protocol flow using authorisation code grant.
The client must implement a **redirection endpoint**, also known as “OAuth 2.0 callback”. This endpoint must be secured by Transport Layer Security (TLS) and must be hosted on a public domain (hence, accessible to all users on the internet). When registering your client, you must provide this redirection URL in the technical information.
**HTTP is allowed for local development**
Use of HTTPS is often inconvenient in local development environment. Therefore, there is an exception for "loopback" or "localhost" redirect URIs, which can use the unencrypted HTTP protocol. Specifically, these hosts can use `http` scheme in redirect URIs:
* `localhost` loopback interface
* `127.0.0.1` IPv4 loopback interface, recommended
* `[::1]` IPv6 loopback interface
Only exact `localhost` host name is supported. Aliases such as `myclient.localhost` or `myclient.local` would not be accepted.
When deploying your client in the production environment, use of **HTTPS** is mandatory.
**Multiple Redirect URIs**
If you intend to support multiple URLs for redirection endpoints, you must provided as a list of registered redirect URIs.
Often, a client allocates resources of their customers in subdomains (or sub-paths). For example: an application might host Alice’s resources at `alice.client.example`, Bob’s resources at `bob.client.example`. Some early implementations of OAuth 2.0 offered this functionality, but later it is diagnosed to be vulnerable to a certain class of attacks on the OAuth 2.0 protocol.
Wildcard redirect URIs are not supported. To comply with best security practices, Pleo authorisation server does not offer any support for wildcards in redirect URIs.
If your client uses wildcards, we recommended using the `state` parameter to save the location your users must be redirected to after completing the OAuth 2.0 flow. In this way, a single redirect endpoint can dispatch users’ browsers to different destinations.
## Testing OAuth 2.0 Using Graphical User Interface (GUI) Applications
During development, it is often beneficial to test the OAuth 2.0 flow, before setting up the client in the production environment. There are a number of popular HTTP GUI applications that offer support to test the OAuth 2.0 authorisation.
Register redirect URIs of your GUI application - if you intend to use a HTTP GUI application for testing the OAuth 2.0 flow and debugging Pleo API calls, ensure that you include the redirect URIs (supported by the GUI applications) in a list of registered redirect URIs for your client.
We recommend using Postman [how to configure and use Postman](/docs/testing-pleo-oauth-and-apis-using-postman) to test OAuth 2.0 and Pleo APIs.
# OAuth 2.0 Implementation Reference Documentation
Source: https://developers.pleo.io/docs/oauth-implementation-reference-documentation
* **[OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)**
Try a library first (see [Implementing OAuth 2.0 Client Using a Library](/docs/implementing-oauth-client-using-a-library)). Due to complexity of OAuth 2.0 protocol and its numerous extensions, we do not recommend implementing OAuth 2.0 clients from scratch.
Due to complexity of OAuth 2.0 protocol and its numerous extensions, we do not recommend implementing OAuth 2.0 clients from scratch. In this topic, we provide an overview of the protocol with example of it in action.
## Obtaining Access Token Using Authorisation Code
The OAuth 2.0 flow (technically, authorisation code grant flow) consists of the following steps:
1. **Initiate authorisation request**: The client creates an **authorisation request** and directs the user's browser to the authorisation endpoint of the Pleo authorisation server.
2. **Granting or denying access**: The Pleo authorisation server confirms whether the user whether would like to be granted or denied access to the resources.
3. **Authorisation response**: If the user would like to access the resources, the Pleo's authorisation server directs the user agent of the resource owner to the client's redirect endpoint and sends an **authorisation response** to the client. If the authorisation is granted, the response includes an authorisation code.
4. **Access token request**: The client validates the authorisation response and generates an **access token request** using the authorisation code provided by Pleo.
5. **Access token response**: The authorisation server replies with an **access token response**, which contains an access token and, optionally, a refresh token.
## Authorisation Request
To initiate an OAuth 2.0 workflow:
1. A client provides an element on the User Interface (UI) - usually, a **Connect to Pleo** button in their application.
2. When a user of the client clicks this button, the OAuth 2.0 client generates and collects several parameters, and generates an **authorisation request**.
3. The user is redirected to the authorisation endpoint of the Pleo authorisation server: `{AUTHORIZATION_SERVER_URL}/oauth/authorize/`
The client generates the authorisation request URI using the following parameters:
| Parameter | Description |
| :------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `response_type` | Must be set to `code`. |
| `client_id` | Pleo provided client identifier after client registration. |
| `redirect_uri` | (Optional) URI of one of client's registered redirection endpoints. |
| `scope` | The scope of access request. Requested scope must form a subset of the scope that was registered for this client. |
| `state` | (Optional) An opaque value used by the client to maintain state between issuing an authorisation request and receiving an authorisation response. |
| `code_challenge` | (PKCE) Code challenge — a value derived from code verifier using the chosen code challenge method. For more information, see [PKCE](/docs/implementing-the-oauth-client-from-scratch). |
| `code_challenge_method` | (PKCE) Code challenge method. Must be set to `S256`. For more information, see [PKCE](/docs/implementing-the-oauth-client-from-scratch) . |
| `urn_pleo_params_oauth_subject` | The subject urn returned as part of the claim in the session handover. |
### PKCE
Parameters marked with (PKCE) are defined in PKCE extension of the core OAuth 2.0 protocol. They are mandatory, except for clients that don’t support PKCE, and were marked as `not supported` during client registration.
PKCE generator. You can use free online tools, such as [https://tonyxu-io.github.io/pkce-generator/](https://tonyxu-io.github.io/pkce-generator/), to generate parameters defined by PKCE extension, for the purposes of testing and validating your OAuth 2.0 client implementation.
For full reference on PKCE, consult [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636).
## Authorisation Response
This describes a typical authorisation response workflow:
1. After receiving an authorisation request, the Pleo authorisation server displays the authorisation page to the user, showing information about the client and details of authorisation request, including the requested permissions.
2. The user could either proceed with the authorisation request or discontinue the request. The result of the choice is conveyed back to the client in an **authorisation response**.
* If the response of the user is to proceed with the authorisation request, Pleo interacts with the resource owner. Then the authorisation server redirects the user agent of the resource owner to one of the previously registered redirect endpoints of the client.
3. The client's redirect endpoint receives an authorisation response as an HTTP request; the authorisation response could either be a successful authorisation of access or an error.
4. After receiving an authorisation response, the client validates it.
* If the authorisation response is valid and represents successful authorisation, the client might send an **access token request** to the Pleo authorisation server. This request contains the **authorisation code** received in the authorisation response.
* If the authorisation response represents an authorisation error (usually, because the end-user denied the authorisation request), or is invalid, the client might show a corresponding error message and might prompt the user to try the authorisation request again.
### Authorisation Granted
If the client is authorised access to the resource owner, a successful **authorisation response** contains the following parameters:
| Parameter | Description |
| :-------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | The authorisation code generated by the Pleo authorisation server. The client uses this authorisation code in the access token request; it can be used only once. |
| `state` | (Required, if the authorisation request contained a `state` parameter) The exact value of state parameter sent by the client in the authorisation request. If this value is present, must be validated by the client before generating an access token request. This can be used by the client to restore state after redirection. |
### Authorisation Denied
If the authorisation response represents an error or is invalid, the client must not generate an access token request, and should display a corresponding error message to the end-user.
The **authorisation response** for an unsuccessful request contains the following parameters.
| Parameter | Description |
| :------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `error` | A single error code, usually `access_denied`. |
| `state` | (Required, if the authorisation request contained a `state` parameter) The exact value of state parameter sent by the client in the authorisation request. |
| `error_description` | (Optional) Description of the error description to assist the developer of the client. |
| `error_uri` | (Optional) An URI identifying a web page containing detailed description of the error. |
## Access Token Request (Authorisation Code Grant)
An access token request is a HTTP request generated directly from the client and sent to the Pleo authorisation server (`{AUTHORIZATION_SERVER_URL}/oauth/token`), containing the following parameters:
| Parameter | Description |
| :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `grant_type` | Must be set to `authorization_code`. |
| `code` | The authorisation code received from the authorisation server in the authorisation response. |
| `redirect_uri` | Required, if the redirect\_uri parameter was included in the authorisation request. If yes, the value sent in this request must be identical with the one sent in the authorisation request. |
| `code_verifier` | (PKCE) A code verifier value that was used to generate a code challenge. |
The access token token request must be authenticated using `client_secret_basic` authorisation method, that is, an Authorization HTTP header, using Basic authorisation scheme and corresponding client credentials (client identifier and client secret).
## Access Token Response
If the access token request is valid, the Pleo authorisation server returns an access token response, providing the following parameters, encoded as a JSON object:
| Parameter | Description |
| :-------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `access_token` | The access token that is issued by the Pleo authorisation server. This token can be used to access endpoints provided by the resource servers. The validity of the access token is mentioned in the response — `expires_in` attribute mentions the lifespan. |
| `token_type` | The type of access token. Only **bearer tokens** are supported by the Pleo authorisation server and resource servers. |
| `expires_in` | The lifetime in seconds of the access token. An access token is valid for the duration specified in the `expires_in`parameter, after which it would be rejected by the resource servers. A new access token can be obtained automatically using a refresh token. |
| `refresh_token` | The refresh token can be used to obtain new access tokens to replace expiring ones. The validity of a refresh token is not defined but is valid for at least **60 days**. After this time span, it is not guaranteed whether the token would be still valid. |
| `scope` | (Optional) The scope of the access token. If not present, it is identical to the scope requested by the client in the authorisation request. |
## Access Token Request (Refresh Token Grant)
For security, access tokens have a relatively short lifespan and cannot be used to access APIs of the resource server after expiration. However, authorisation server may issue a refresh token along with the access token. A refresh token can be used to obtain a new access token to replace an expiring one.
This is a typical workflow of a refresh access token request:
1. The client detects that the access token is about to expire.
2. The client performs an access token request using refresh token grant.
3. The authorisation server replies with an access token response, which contains a new access token, and, optionally, a new refresh token.
4. The client discards the old access token and the old refresh token.
An access token request using refresh token grant uses the following parameters:
| Parameter | Value |
| :-------------- | :-------------------------------------------------------- |
| `grant_type` | Must be set to `refresh_token`. |
| `refresh_token` | Refresh token received in the last access token response. |
The access token response contains a new access token and a new refresh token. After receiving an access token response, the client must discard the old access and the refresh tokens, and use the new ones instead.
If your refresh token has expired and you are using the same to refresh it, all active refresh tokens are invalidated. This is done to prevent any possible replay attack.
## Example
The example used in this section utilises the registration data of the client, as described in [OAuth 2.0 client registration](/docs/client-registration), and illustrates the entire workflow of generating an OAuth 2.0 authorisation code grant, as well as refreshing an access token using the refresh token grant.
### Authorisation Request
A client implements a web page that serves as a starting point for initiating the authorisation with Pleo. In our example, this web page displays **Connect to Pleo** button. When a user clicks this button, the OAuth 2.0 client library is activated.
The OAuth 2.0 client library then fills the parameters to generate an authorisation request with the following parameters:
| Parameter | Example value | Notice |
| :------------------------------ | :---------------------------------------------------- | :------------------------------------------------------------------------------------------------- |
| `response_type` | `code` | Always `code`. |
| `client_id` | `36e3b610-56d7-4d36-92c7-a003ca7bfc5f` | Client identifier received during registration. |
| `redirect_uri` | `https://client.example/callback` | One of the redirect URIs registered by this client. |
| `scope` | `test:test users:read` | A subset of scopes registered for use by this client. |
| `state` | `d5a2d4566e51a28ecb3b58841b39df` | An opaque value which helps restore the client context later. |
| `code_verifier` (not sent) | `wo8H_PzaG9eH6_wycgwJmGcYG-wdEkm5VulQBCJvA7I` | (PKCE) Generated randomly, only used once per each authorisation request. Kept secret until later. |
| `code_challenge` | `bV7Y93L9KPvF-1R0TN2iDeZrHEm2D5OflR3O_Hf5oRQ` | (PKCE) Generated from `code_verifier` using `S256` code challenge method. |
| `code_challenge_method` | `S256` | (PKCE) Always `S256`. |
| `urn_pleo_params_oauth_subject` | urn:pleo:company:123e4567-e89b-12d3-a456-426614174000 | The subject urn returned as part of the claim in the session handover. |
Mapping between these parameters and the configuration options provided by OAuth 2.0 client implementation, depend on the choice of the the software. Consult the documentation provided by your chosen implementation of OAuth 2.0 client.
The OAuth 2.0 library then performs a HTTP redirect to the Pleo authorisation server, thus executing an authorisation request. This can be done, for example, using HTTP 302 status code with the `Location` header containing the URL of the Pleo authorisation endpoint with all the parameters.
An example of a HTTP redirect from the client to the authorisation server:
```bash HTTP theme={null}
HTTP/1.1 302 Found
Location: https://auth.pleo.io/oauth/authorize
?response_type=code
&client_id=36e3b610-56d7-4d36-92c7-a003ca7bfc5f
&redirect_uri=https%3A%2F%2Fclient.example%2Fcallback
&scope=test%3Atest+users%3Aread
&state=d5a2d4566e51a28ecb3b58841b39df
&code_challenge=bV7Y93L9KPvF-1R0TN2iDeZrHEm2D5OflR3O_Hf5oRQ
&code_challenge_method=S256
```
### Authorisation Response
The user’s browser is redirected to the Pleo authorisation server, which performs the necessary user authentication and interacts with the resource owner to authorise the “Example Client”. If the user is granted authorisation, the authorisation server redirects the user’s browser back to the client, specifically to the redirect endpoint of the client.
```bash HTTP theme={null}
HTTP/1.1 302 Found
Location: https://client.example/callback
?code=SplxlOBeZQQYbYS6WxSbIA
&state=d5a2d4566e51a28ecb3b58841b39df
```
The client redirect endpoint is controlled by the OAuth 2.0 client library, which can then proceed to generate an access token request with the parameters it received in the authorisation response.
| Parameter | Received value | Notice |
| :-------- | :------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | `SplxlOBeZQQYbYS6WxSbIA` | Authorisation code issued by the authorisation server. This authorisation code is used in the access token request. The code is for single-use only. |
| `state` | `d5a2d4566e51a28ecb3b58841b39df` | Opaque value from the authorisation request sent back by the authorisation server. Must be recognised and validated by the client. |
If `state` is present, the client compares it to a known value that it has sent during the authorisation request, to make sure they match. It can also use this value to restore its own state.
### Access Token Request (Authorisation Code Grant)
The OAuth 2.0 client library performs an access token request: an authenticated request to receive an access token in exchange of the authorisation code received in the authorisation response.
The access token request rely on client authentication, which is performed using the `Basic` HTTP authentication scheme, defined in [RFC 7617](https://www.rfc-editor.org/rfc/rfc7617).
The following parameters are included in an access token request:
| Parameter | Example value | Notice |
| :-------------- | :-------------------------------------------- | :--------------------------------------------------------------------------------------------------------- |
| `grant_type` | `authorization_code` | Always `authorization_code` in authorisation code grant. |
| `code` | `SplxlOBeZQQYbYS6WxSbIA` | Authorisation code received in the authorisation response. |
| `redirect_uri` | `https://client.example/callback` | The same redirect URI as the one that was used in the authorisation request. |
| `code_verifier` | `wo8H_PzaG9eH6_wycgwJmGcYG-wdEkm5VulQBCJvA7I` | (PKCE) Code verifier generated before the authorisation request, now revealed to the authorisation server. |
```bash HTTP theme={null}
POST /oauth/token HTTP/1.1
Host: auth.pleo.io
Authorization: Basic MzZlM2I2MTAtNTZkNy00ZDM2LTkyYzctYTAwM2NhN2JmYzVmOjcwNzcxZjNjYmY0NzJiYTkxNmFlZmQyMWJlOWM3YQ==
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Fcallback
&code_verifier=wo8H_PzaG9eH6_wycgwJmGcYG-wdEkm5VulQBCJvA7I
```
* The request parameters are sent using `application/x-www-form-urlencoded` content type, and not `application/json`.
* Mapping between these parameters and the configuration options provided by OAuth 2.0 client implementation, depend on the choice of the the software. Consult the documentation provided by your chosen implementation of OAuth 2.0 client.
### Access Token Response
After receiving and validating the access token request, the authorisation server responds back to the client with the access token response.
```bash HTTP theme={null}
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token": "MTZhNjExbTR2MXI0bjRiNDgyMjZrOTU4NTg2YzNl",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"
}
```
The client can now can perform API requests using the access token. In addition, the client stores the access token and observes its lifespan. Once expired, the old access and the refresh tokens are replaced by the new one (or discarded, if no new refresh token is present).
### Access Token Request (Refresh Token Grant)
A new access token can be obtained to replace an expiring one, using a refresh token grant:
| Parameter | Value | Notice |
| :-------------- | :----------------------- | :------------------------------------------------------------------------------------ |
| `grant_type` | `refresh_token` | Always `refresh_token` in refresh token grant. |
| `refresh_token` | `tGzv3JOkF0XG5Qx2TlKWIA` | Refresh token received in the last access token response. This can be used only once. |
```Text HTTP theme={null}
POST /oauth/token HTTP/1.1
Host: auth.pleo.io
Authorization: Basic MzZlM2I2MTAtNTZkNy00ZDM2LTkyYzctYTAwM2NhN2JmYzVmOjcwNzcxZjNjYmY0NzJiYTkxNmFlZmQyMWJlOWM3YQ==
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
```
After receiving a new access token and, optionally, a new refresh token, the client must discard the old ones.
### Authorisation Server URL
When configuring authorisation and token endpoints, substitute the `{AUTHORIZATION_SERVER_URL}` with the base URL of the Pleo authorisation server in the environment that you are developing for.
| Environment | Authorisation Server URL |
| :---------- | :----------------------------- |
| Staging | `https://auth.staging.pleo.io` |
| Production | `https://auth.pleo.io` |
## Reference Documentation
For brevity, we omit minor details. For full reference, consult the following documents:
* D. Hardt, [**The OAuth 2.0 Authorization Framework**](https://www.rfc-editor.org/rfc/rfc6749.html), RFC 6749, October 2012
* M. Jones, D. Hardt, [**The OAuth 2.0 Authorization Framework: Bearer Token Usage**](https://www.rfc-editor.org/rfc/rfc6750.html), RFC 6750, October 2012
* J. Reschke, **[The 'Basic' HTTP Authentication Scheme](https://www.rfc-editor.org/rfc/rfc7617)**, RFC 7617, September 2015
* N. Sakimura, Ed., [**Proof Key for Code Exchange by OAuth Public Clients**](https://www.rfc-editor.org/rfc/rfc7636), RFC 7636, September 2015
# Pleo APIs by Solution
Source: https://developers.pleo.io/docs/pleo-apis-by-solution
* **[API Overview](/docs/current/getting-started/platform-api-overview)**
* **[Platform Overview](/docs/current/getting-started/platform-overview)**
Here is a list of Pleo APIs and the solutions they provide:
| Pleo API | Solution |
| :------------------------ | :----------------------------------------------------------------------------------------------------------------------- |
| Export API | Export accounting entries from Pleo to an external ERP/accounting system |
| Tags API | Assign tags (cost centres) to accounting entries. |
| Tax Code API | Manage tax codes of a company in Pleo. |
| Webhook Subscriptions API | Manage webhook subscriptions for events generated in Pleo and receive real-time notifications for the subscribed events. |
| Employees API | Search employee details of an organisation/company in Pleo. |
| App Marketplace API | Connecting Pleo with third-party applications listed in the app marketplace. |
# Pleo API Overview
Source: https://developers.pleo.io/docs/pleo-introduction
* **[API Overview](/docs/current/getting-started/platform-api-overview)**
* **[Platform Overview](/docs/current/getting-started/platform-overview)**
Welcome to the Pleo API 👋
[Pleo](https://www.pleo.io/en) is an expense management solution that helps your organisation with all aspects of business spending. We help you to simplify expense reporting and automate financial workflows; thus saving your time and effort in managing and bookkeeping accounting entries.
Our REST API is your toolkit for building bespoke integrations and automating workflows. It is defined using the OpenAPI specification, uses standard HTTP verbs and accepts and returns JSON.
### What you can build
Here are a few ways you can use the APIs:
* **Automate your bookkeeping**: Export accounting data from Pleo into your accounting or ERP system, including tax codes and receipts.
* **Keep your cost centres in sync**: Sync cost centers/dimensions between Pleo and the ERP system.
* **Streamline user management**: Synchonize with your HR system to automate the adding or removal of user from Pleo and the provisioning of cards
* **Build integrations**: Extend Pleo's capabilities by integrating it with the tools you use and publish them onto our [App Marketplace](https://www.pleo.io/en/integrations).
### Getting started
Ready to start building? Our [Quickstart guide](/docs/quickstart-guide) is the best place to begin.
### Quick links
* [Quickstart guide](/docs/quickstart-guide)
* [Authentication](/docs/authentication-authorisation-introduction)
* [Learn more about Pleo](/docs/learn-more-with-pleo)
* [App Marketplace](https://www.pleo.io/en/integrations)
### Support
If you need help please send us an email at [api@pleo.io](mailto:api@pleo.io)
# Quickstart
Source: https://developers.pleo.io/docs/quickstart-guide
* **[OAuth 2.0 Setup Workflow Guide](/docs/current/guides/oauth-workflow-guide)**
* **[OAuth 2.0 Setup with Postman Workflow Guide](/docs/current/guides/oauth-workflow-guide-postman)**
* **[API Keys Workflow Guide](/docs/current/guides/standalone-api-keys-workflow-guide)**
Welcome to the Pleo API! We're here to help you get connected fast. This Quickstart guide walks you through using an API key to get up and running.
### Introduction
There are two ways to authenticate:
* [OAuth 2.0](/docs/oauth-20)
* [API keys](/docs/api-keys)
For this guide, we will be using the **API keys flow** and assume you have a Pleo account.
### 1. Get your API key
1. Head to your Pleo account > Settings > Scroll down and click on [API keys](https://app.pleo.io/settings/api-keys)
2. Click on *Create API key*
3. Add the following details
* **Name**: give your key a name that will help you remember what it's for, like `Pleo API Quickstart`
* **Expiration date**: choose when you want the key to expire
* **Access level**: decide if the key is for a specific entity (company) or for multiple entities of a business (organisation). [Read about the differences here](/docs/multi-entity-setup-with-pleo).
* **Permissions**: select the scopes you need. Your selections will automatically enable the right permissions in your key.
4. Click on *Create API Key*
5. Store your key securely as you will only be able to see it once
### 2. Make your first API call
Time to put your new key to use!
When authenticating with API keys, include the key in the `Authorization` header of the request, using the `Basic` schema. The key should be used as the username, and an empty string should be used as the password.
We'll use the [Get Search for employees](/reference/employees/search-for-employees) query for this example.
**curl**
With curl, specify the `-u / --user` flag, followed by the API key. The password is an empty string.
```shell Request example theme={null}
curl --request GET \
--url https://external.pleo.io/v2/employees \
--header 'accept: application/json;charset=UTF-8' \
--header 'authorization: Basic API_KEY'
```
If the request is successful, you will receive a response such as:
```json Response example theme={null}
{
"data": [
{
"code": "string",
"companyId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "string",
"firstName": "string",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"jobTitle": "string",
"lastName": "string",
"phone": "string"
}
],
"pagination": {
"currentRequestPagination": {
"after": "string",
"before": "string",
"limit": 0,
"offset": 0,
"parameters": {
"additionalProp": [
"string"
]
},
"sortingKeys": [
"string"
],
"sortingOrder": [
"ASC"
]
},
"endCursor": "string",
"hasNextPage": true,
"hasPreviousPage": true,
"startCursor": "string",
"total": 0
}
}
```
**Postman**
In Postman, you can specify the API key in the Authorization tab, selecting the Basic Auth type, and using the API key as the username, and an empty string as the password.
### Errors
We use standard HTTP status codes to let you know if a request was successful or if there was an issue. For a full list of codes and what they mean, check out our [API Response Codes | Errors guide](/reference/api-response-codes-errors-1).
### Next steps
Congrats, you've just completed the Quickstart 🎉 We recommend checking the [Authorization guide](/docs/authentication-authorisation-introduction) next.
**What’s Next**
[Introduction ->](/docs/authentication-authorisation-introduction)
# Terms and Definitions
Source: https://developers.pleo.io/docs/terms-and-definitions
A reference for the key terms used throughout Pleo's documentation and APIs.
Core concepts and terminology used across Pleo.
All expense types and subtypes returned by the Pleo Export API.
Terms used when exporting accounting entries from Pleo to an external ERP or accounting system.
# Session Handover
Source: https://developers.pleo.io/guides/integrations/session-handover
# Introduction
In the case where integrations are third-party applications, meaning they are not built as part of Pleo or the target system, the integration would either have to keep its own login screen, which would be a suboptimal user experience, or rely on a "session handover" from Pleo.
To solve for this, our solution consists of a custom process where Pleo will generate a JWT token that we will pass along with the request. The third-party application can then verify the token by validating the token.
If the user isn't authorized to manage the integration, the Pleo apps will manage the user experience, which means that the integration can assume that any request coming in with a valid token is authorized.
Once the token is verified, the integration can create a local session for the user and show the integration, essentially treating the handover as a successful login.
Session handover token != access token. The session handover token is not an access token. It is a token that is used to verify that the user is authorized to manage the integration. The integration should not use the token to access the Pleo API.
## General flow
1. From within Pleo, the User clicks on "Enable" or "Configure" for the integration
2. Pleo creates and signs a JWT containing key information such as
* Company ID
* User ID
* Locale
* Expire date
3. Pleo redirects the user to the integration with the JWT token attached in a query param (`pleo_id`)
4. The integration verifies the JWT. **Please note that before the token is confirmed valid, no information in the token should be trusted.**
## Implementation
As much as possible, this spec leans onto standards defined by OpenID Connect **\[[OpenID.Core](http://openid.net/specs/openid-connect-core-1_0.html)]** set of specifications, for ease of comprehension, implementation, and migration.
## ID Token
### Header Parameters
The Header Parameters for JWT ID Token were chosen to simplify implementation, and mitigate possible token misuse and token substitution attacks.
The header MUST contain following Header Parameters:
| Header Parameter | Description |
| :--------------- | :------------------------------------------------------------- |
| alg | Cryptographic algorithm used to secure the JWT. MUST be RS256. |
| typ | Media type. MUST be pleo\_id+jwt. |
| kid | Key ID. Indicates which key was used to secure the JWT. |
Example:
```json theme={null}
{
"alg": "RS256",
"typ": "pleo_id+jwt",
"kid": "sig-1696245492"
}
```
### Mandatory Claims
The following claims are mandatory and MUST be present on all ID Tokens.
| Claim | Description |
| :---- | :-------------------------------------------------------------------- |
| iss | Issuer Identifier. MUST be a URL of an issuer controlled by Pleo. |
| sub | Subject. Contains the ID of the End-User represented by the ID token. |
| aud | Audience. MUST be the Client ID of the OAuth Client. |
| exp | Expiration time as a UNIX timestamp. |
| iat | The time the token was issued as a UNIX timestamp. |
A Client receiving the ID Token MUST validate these claims before using the token, as described in [ID Token validation](#IDTokenValidation) section below.
Example:
```json theme={null}
{
"iss": "https://auth.pleo.io",
"sub": "04fbc415-e5fc-4acc-937c-8964747ad43c",
"aud": "67e70bba-088d-47c7-a542-e631bb8cca7f",
"exp": 1696242931,
"iat": 1696239331
}
```
### End-User Claims
The user personal information included in the claims.
| Claim | Required | Description |
| :----------- | :------- | :---------------------------------------------------------------------------------------------------- |
| sub | REQUIRED | Subject. The ID of a user as used in Pleo APIs. |
| name | | Full name, including any titles and suffixes. |
| given\_name | | Given name(s) or first names(s). |
| family\_name | | Family name(s) or last names(s). |
| locale | | End-User's locale, represented as a BCP47 [RFC5646](http://tools.ietf.org/html/rfc5646) language tag. |
```json theme={null}
{
"sub": "04fbc415-e5fc-4acc-937c-8964747ad43c",
"name": "Jeppe Carøe Rindom",
"given_name": "Jeppe",
"family_name": "Rindom",
"locale": "da-DK"
}
```
### Additional Claims
Under the `urn:pleo` namespace you'll find additional claims.
| Claim | Description |
| :--------------- | :------------------------- |
| urn:pleo:company | Company Information Claim. |
#### Company Information Claim
The Company Information Claim represents a legal entity.
| Field | Required | Description |
| :------ | :------- | :------------------------------------------------ |
| sub | REQUIRED | Subject. The Company ID, as used by the Pleo API. |
| name | | Company name. |
| address | | Legal address. |
Example:
```json theme={null}
{
"urn:pleo:company": {
"sub": "3f4d3cf9-806f-4f6f-8cb0-94b69d23109e",
"name": "Pleo Technologies A/S",
"address": {
"formatted": "Ravnsborg Tværgade 5 C, 4. Copenhagen N, 2200, Denmark",
"street_address": "Ravnsborg Tværgade 5 C",
"locality": "Copenhagen",
"postal_code": "2200",
"country": "Denmark"
}
}
}
```
## []()ID Token validation
A Client receiving the ID Token MUST validate it before making use of any of the Claims that it contains. For reference, see [Section 3.1.3.7 ID Token Validation](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation) of **\[[OpenID.Core](http://openid.net/specs/openid-connect-core-1_0.html)]**, from which the following algorithm was adapted.
1. JWT Header Parameters
The `typ` JWT Header Parameter MUST be `pleo_id+jwt`.
The `alg` value of JWT Header Parameter MUST be `RS256`.
The public key identified by the `kid` Header Parameter MUST be present in public key set published in JWKS (see JWKS locations below) format by the Issuer.
The Client MUST validate the signature of all ID Tokens according to JWS **\[[RFC7515](https://datatracker.ietf.org/doc/html/rfc7515)]** using the algorithm specified in the JWT `alg` Header Parameter. The Client MUST use the keys provided by the Issuer.
2. JWT Payload
The Issuer Identifier presented by `iss` Claim MUST be a URL that belongs to a Pleo-controlled Issuer trusted by the Client.
The Client MUST validate that the `aud` (audience) Claim contains its `client_id` value registered at the Issuer identified by the `iss` (issuer) Claim as an audience.
The current time MUST be before the time represented by the `exp` Claim.
The `iat` Claim MAY be used to reject tokens that were issued too far in the past from the current time.
When comparing timestamps, some small leeway MAY be allowed to account for clock skew.
If during verification any of the “MUST” conditions are broken, the Client MUST reject the ID Token, and MUST NOT use any of the values represented in its Claims.
### JWKS location
| Issuer | Environment | JWKs Config URI |
| :----------------------------------------------------------- | :---------- | :------------------------------------------------------------------------------------------------------- |
| [https://auth.staging.pleo.io](https://auth.staging.pleo.io) | staging | [https://auth.staging.pleo.io/.well-known/jwks.json](https://auth.staging.pleo.io/.well-known/jwks.json) |
| [https://auth.pleo.io](https://auth.pleo.io) | production | [https://auth.pleo.io/.well-known/jwks.json](https://auth.pleo.io/.well-known/jwks.json) |
## Example of a full ID Token
Following is an example of a full ID Token, with line breaks for illustrative purposes only.
```
eyJhbGciOiJSUzI1NiIsInR5cCI6InBsZW9faWQrand0Iiwia2lkIjoic2l
nLTE2OTYyNDU0OTIifQ
.
eyJpc3MiOiJodHRwczovL2F1dGgucGxlby5pbyIsInN1YiI6IjA0ZmJjNDE
1LWU1ZmMtNGFjYy05MzdjLTg5NjQ3NDdhZDQzYyIsImF1ZCI6IjY3ZTcwYm
JhLTA4OGQtNDdjNy1hNTQyLWU2MzFiYjhjY2E3ZiIsImV4cCI6MTY5NjI0M
jkzMSwiaWF0IjoxNjk2MjM5MzMxLCJuYW1lIjoiSmVwcGUgQ2Fyw7hlIFJp
bmRvbSIsImdpdmVuX25hbWUiOiJKZXBwZSIsImZhbWlseV9uYW1lIjoiUml
uZG9tIiwibG9jYWxlIjoiZGEtREsiLCJ1cm46cGxlbzpjb21wYW55Ijp7In
N1YiI6IjNmNGQzY2Y5LTgwNmYtNGY2Zi04Y2IwLTk0YjY5ZDIzMTA5ZSIsI
m5hbWUiOiJQbGVvIFRlY2hub2xvZ2llcyBBL1MiLCJhZGRyZXNzIjp7ImZv
cm1hdHRlZCI6IlJhdm5zYm9yZyBUdsOmcmdhZGUgNSBDLCA0LiBDb3Blbmh
hZ2VuIE4sIDIyMDAsIERlbm1hcmsiLCJzdHJlZXRfYWRkcmVzcyI6IlJhdm
5zYm9yZyBUdsOmcmdhZGUgNSBDIiwibG9jYWxpdHkiOiJDb3BlbmhhZ2VuI
iwicG9zdGFsX2NvZGUiOiIyMjAwIiwiY291bnRyeSI6IkRlbm1hcmsifX19
.
oSk73ScKYTji8SssmlXmLxF2uFFFMYKFs3VWeug1HSk7ilQgWs0N1dask2m
ngVKrZIRPLYLFJnKYH83Ywua52Y63QFjHlTrLytLkvIcXMHEaQYNGEBJJ-6
dM8qBsHULxyUO6lhTDgBzdddgcpX2NmE9iJlw3wajsedatui3uazuAZvbTz
dSjSJIXNzIUCxG18X4pWn6n4GqbAxzfjpLQcAa8G_-nYjf51iK2egGEymG6
WhyvTyf0C0nDH9uWnJCiDDNbncTdlA1_XgrEaUyMVNe0Nt04t9TIffzweYx
U1NQgIm19DSila3ic58mH5WKQbO4Su-UuEhx4Ad3hzfOQqw
```
[Examine this example on jwt.io](https://jwt.io/#debugger-io?token=eyJhbGciOiJSUzI1NiIsInR5cCI6InBsZW9faWQrand0Iiwia2lkIjoic2lnLTE2OTYyNDU0OTIifQ.eyJpc3MiOiJodHRwczovL2F1dGgucGxlby5pbyIsInN1YiI6IjA0ZmJjNDE1LWU1ZmMtNGFjYy05MzdjLTg5NjQ3NDdhZDQzYyIsImF1ZCI6IjY3ZTcwYmJhLTA4OGQtNDdjNy1hNTQyLWU2MzFiYjhjY2E3ZiIsImV4cCI6MTY5NjI0MjkzMSwiaWF0IjoxNjk2MjM5MzMxLCJuYW1lIjoiSmVwcGUgQ2Fyw7hlIFJpbmRvbSIsImdpdmVuX25hbWUiOiJKZXBwZSIsImZhbWlseV9uYW1lIjoiUmluZG9tIiwibG9jYWxlIjoiZGEtREsiLCJ1cm46cGxlbzpjb21wYW55Ijp7InN1YiI6IjNmNGQzY2Y5LTgwNmYtNGY2Zi04Y2IwLTk0YjY5ZDIzMTA5ZSIsIm5hbWUiOiJQbGVvIFRlY2hub2xvZ2llcyBBL1MiLCJhZGRyZXNzIjp7ImZvcm1hdHRlZCI6IlJhdm5zYm9yZyBUdsOmcmdhZGUgNSBDLCA0LiBDb3BlbmhhZ2VuIE4sIDIyMDAsIERlbm1hcmsiLCJzdHJlZXRfYWRkcmVzcyI6IlJhdm5zYm9yZyBUdsOmcmdhZGUgNSBDIiwibG9jYWxpdHkiOiJDb3BlbmhhZ2VuIiwicG9zdGFsX2NvZGUiOiIyMjAwIiwiY291bnRyeSI6IkRlbm1hcmsifX19.oSk73ScKYTji8SssmlXmLxF2uFFFMYKFs3VWeug1HSk7ilQgWs0N1dask2mngVKrZIRPLYLFJnKYH83Ywua52Y63QFjHlTrLytLkvIcXMHEaQYNGEBJJ-6dM8qBsHULxyUO6lhTDgBzdddgcpX2NmE9iJlw3wajsedatui3uazuAZvbTzdSjSJIXNzIUCxG18X4pWn6n4GqbAxzfjpLQcAa8G_-nYjf51iK2egGEymG6WhyvTyf0C0nDH9uWnJCiDDNbncTdlA1_XgrEaUyMVNe0Nt04t9TIffzweYxU1NQgIm19DSila3ic58mH5WKQbO4Su-UuEhx4Ad3hzfOQqw\&publicKey=%7B%0A%20%20%20%20%22kty%22%3A%20%22RSA%22%2C%0A%20%20%20%20%22e%22%3A%20%22AQAB%22%2C%0A%20%20%20%20%22use%22%3A%20%22sig%22%2C%0A%20%20%20%20%22kid%22%3A%20%22sig-1696245492%22%2C%0A%20%20%20%20%22alg%22%3A%20%22RS256%22%2C%0A%20%20%20%20%22n%22%3A%20%22s0D3uFrxs7wDFS1GXezWUQ28KwTtHH5XuHlltF6R7JCMb3UML2GbfZHrLzB5Jy0o78CYuRTBHXeAqHv4FOW_ffiLMrOxHly9agDcjB99kHFy6x6D52vQzsP-KFmFwEx2vO6x9uWo_f6Af_eOXEUafm6tZGQWWxtTDF73ldlb6ugIxsIMSeCTxRia21PZeYhdyrtNNx8bTM9j2HRgqsQ_kfymM54bsp37jFiV7bgJKkdUzLIYwq-9vHzw33q-hvLSvu7iG4BYcvKhAxKUtzJGmISl9CosJIxF30igJUhqCtYASY1mQDZdSVu7FQiylUVBbbrwUU9JQFmwZnD5FYW69w%22%0A%7D)
### JWT Header
```json theme={null}
{
"alg": "RS256",
"typ": "pleo_id+jwt",
"kid": "sig-1696245492"
}
```
This represents the same header as was used as an example in the normative section. It uses the public key identified by `sig-1696245492` Key ID for signature verification using `RS256` algorithm.
### JWT Payload
```json theme={null}
{
"iss": "fa",
"sub": "04fbc415-e5fc-4acc-937c-8964747ad43c",
"aud": "67e70bba-088d-47c7-a542-e631bb8cca7f",
"exp": 1696242931,
"iat": 1696239331,
"name": "Jeppe Carøe Rindom",
"given_name": "Jeppe",
"family_name": "Rindom",
"locale": "da-DK",
"urn:pleo:company": {
"sub": "3f4d3cf9-806f-4f6f-8cb0-94b69d23109e",
"name": "Pleo Technologies A/S",
"address": {
"formatted": "Ravnsborg Tværgade 5 C, 4. Copenhagen N, 2200, Denmark",
"street_address": "Ravnsborg Tværgade 5 C",
"locality": "Copenhagen",
"postal_code": "2200",
"country": "Denmark"
}
}
}
```
In this example:
* In the `sub` claim you find `04fbc415-e5fc-4acc-937c-8964747ad43c` which is the ID of a resource representing the user by the name of Jeppe Rindom
* In the `aud` claim you find `67e70bba-088d-47c7-a542-e631bb8cca7f` which is the `client_id` of a hypothetical Client that receives this token
* In the `sub` claim under `urn:pleo:company` you find `3f4d3cf9-806f-4f6f-8cb0-94b69d23109e` which is the ID of a resource representing “Pleo Technologies A/S” company
These values are for illustrative purposes only.
# Build with Pleo
Source: https://developers.pleo.io/index
Developer Portal Documentation
# Build with Pleo
Whether you're connecting your own business to Pleo or building an integration as a Pleo Partner, this is where you'll find everything you need to get started.
The core capabilities and concepts behind an integration with Pleo.
How expense data moves from Pleo into your Accounting System: bookkeeping methods, accounts mapping, data mapping, and posting behaviour.
How Tags, Chart of Accounts, and Tax Codes stay synchronised from your Accounting System into Pleo, ready to assign to expenses.
Every endpoint Pleo exposes: Export, Tags, Tax Code, Accounting Entries, Employees, Webhooks, Marketplace APIs, and more.
## Start integrating
Connect your own Accounting System, or build for many customers as a Pleo Partner.
Understand the four integration levels, from exporting expense data (Level 1) through to bi-directional Vendor sync (Level 4), and find out where to start.
## Popular catalogues
Browse everything available at each layer of the accounting integration documentation.
The rules and requirements behind each part of the integration: what your integration must do, and what Pleo expects.
End-to-end sequences for each integration level, showing how every step connects before you start writing code.
Step-by-step articles for each stage of implementation, with example requests and links to the relevant API reference.
## Pleo MCP
Connect an AI assistant or agent to your Pleo data and manage expenses by talking to it.
What the Pleo MCP is, how the agent-to-Pleo flow works, and what you can do with it today.
## Migrating to newer APIs
Find migration pathways for legacy Pleo APIs nearing end of life, and know what to expect.
Guides covering available migration pathways as legacy APIs are retired, with end-of-life dates communicated in advance.
## Stay up to date
Keep track of platform changes and uptime.
See what's new, updated, or fixed across the Pleo APIs.
Check real-time uptime and incident history for Pleo's platform.
## Explore the Pleo ecosystem
Beyond the developer portal, discover more ways to work with Pleo.
Add Pleo's spend management directly into your own platform, white-labelled for your customers.
Browse ready-made integrations available on the Pleo App Marketplace.
Get help with your Pleo account from the support Help Center.
See Pleo in action with an interactive product walkthrough.
Learn more about Pleo's spend management platform.
# Accounting Integration Guide
Source: https://developers.pleo.io/page/accounting-integration-guide
* [AS/ERP (Accounting) Integration Overview](/docs/current/getting-started/accounting-integrations-overview)
* [Export Integration Workflow Guide](/docs/current/guides/export-integration-workflow-guide)
* [Integration Design - Exports Overview](/docs/current/integration-design/exports/integration-design-exports-overview)
This topic helps you to build an ERP/accounting integration with Pleo. Ensure that you have all the information required, before you initiate the process of configuring the integration.
## Prerequisites
Before you start setting up the integration, it is important to know the following:
* **Understanding Pleo:** It is crucial to understand what Pleo does and how it aligns with the integration you have planned. To get started with your understanding, refer to this [section](/docs/pleo-introduction).
* **Authentication and Authorisation:** Pleo supports both OAuth 2.0 and API Keys for authorisation. Select the appropriate method, based on what your client supports. To know further, refer [Authentication and Authorisation](/docs/authentication-authorisation-introduction).
* **Target audience:** Note that only users with admin privileges and bookkeepers are allowed to use this integration.
## Technical Decisions
You must decide the following:
* [Integration Level](/page/accounting-integration-guide#integration-level) : Select the appropriate level of integration, based on what you want your expected outcome to be.
* [Integration Deployment](/page/accounting-integration-guide#integration-deployment): Determine where you want to deploy the integration — on a cloud platform or on premises.
## Integration Level
Determine the level of integration you require, based on what APIs you want the integration to access:
| Level | Export | Tags | Categories | Vendors | Corresponding APIs |
| :------ | :----- | :--- | :--------- | :------ | :---------------------------------------------------------------- |
| Level 1 | ✅ | | | | Export API |
| Level 2 | ✅ | ✅ | | | Export API + Tags API |
| Level 3 | ✅ | ✅ | ✅ | | Export API + Tags API + Tax Code API |
| Level 4 | ✅ | ✅ | ✅ | ✅ | Export API + Tags API + Tax Code API + Vendors API + Accounts API |
\*\* The Vendors API and the Accounts API would be released in 2025.
* **Level 4** is the desired goal and the optimum solution that enables you to set up automated workflows and frequent updates are communicated to the ERP/accounting system.
* **Level 3** enables you to seamlessly transfer data related to categories and taxes from Pleo to the ERP/accounting system.
* **Level 2** is the minimum integration level we recommend in order to support automated workflows between Pleo and the target ERP/accounting software.
## Integration Deployment
You have the following options to deploy your integration (for more information, see [Integration Deployment](/docs/connecting-pleo-with-erpaccounting-system#deployment-of-pleo-integration-with-erpaccounting-system):
* **Module or on-premises:** The integration would be deployed in the on-premises infrastructure of the client. For example, NetSuite and SAP B1 provide a platform in their environment for hosting the integration. In this option, there is only a single step in authorisation — users needs authorisation from the Pleo server to access the Pleo APIs. Also, no additional frontend development is required as the existing frontend of the ERP/accounting system is used for configuring the integration.
* **Cloud platform:** If on-premises is unavailable or not supported by your client, deploy the integration on a cloud platform. In this option, there is a two-steps procedure to complete the authentication and authorisation process:
In the **Settings > Accounting** page of Pleo, the users connect to the ERP/accounting application. Through a smooth session handover, they are redirected to the integration configuration page that is deployed on the cloud platform. The users enable and authenticate the integration on this separate interface.
After the session handover, the users are redirected to the Pleo authorisation server, where they are authorised to access the Pleo APIs.
## Integration Capabilities
To utilise Pleo optimally as the spend management platform, ensure that the target ERP/accounting system supports a few capabilities. Hence, before you start with the integration set up, check whether the ERP/accounting system you are planning to integrate with supports the following capabilities:
* **Bookkeeping decisions and logic:** How and which specific expenses would be bookkept as Journal Entries/Vendor Bills/Expense Reports? For example, **out of pocket** expenses might be bookkept as vendor **bills** and **reimbursements** might be recorded as **journal entries**. Hence, check the accounting practices of the customer and whether the capabilities are supported by the ERP/accounting system. These types are not exhaustive, yet they are the most commonly used accounting practices; based on the customers accounting practices, configure appropriate integration configurations.
* **Uploading receipts to the ERP/accounting application:** This is a mandatory requirement; the users must be able to upload attachments to the accounting entries and the integration would support the transition of these entries to the ERP/accounting system. However, there could be rare exceptional scenarios where systems like Netsuite have this feature as an optional functionality as they might be recording their receipts in another solution. So, if you have a use case that does not require this capability, the target ERP/accounting system might have the exception.
* **Exporting accounting entries to the ERP/accounting system during closed accounting period:** Most businesses often encounter situations where multiple accounting entries are missing crucial information and hence, cannot be bookkept within the corresponding accounting period. But owing to compliance regulations, they have to close the books of accounts for the specific accounting period and cannot make further edits after a specific timeline. These accounting entries must be adjusted in the next cycle. So, the ERP/accounting system must support inclusion of these accounting entries in the next accounting period.
* **Bookkeeping with employee accounts:** This is an optional requirement — some business require tracking of expenses at the employee level. To cater to this requirement, the ERP/accounting system must support bookkeeping of expenses per employee, based on the `employee code` mentioned in the accounting entries. When you are configuring the integration, you can specify the type of expenses for which tracking is required at the employee level, for example, **out of pocket** expenses, **reimbursements**, and **ATM transactions** require managing at the employees' level; hence, when these accounting entries are exported to the ERP/accounting system, the expenses would be book kept per employee.
In your Pleo account, on the People page, you can assign employee codes to the employee records. This help in managing expenses at the employee levels.
* **Customising accounting entry description:** This is an additional optional capability. Some businesses need a summarised description of accounting entries; the description is a concatenation of other important fields and aids in providing the required information without clicking the details. For example, the description might contain the vendor name, employee information, and the category assigned to the accounting entry. If the business has other requirements, the description might also contain other details.
## Exporting Accounting Entries
The primary intent of any ERP/accounting integration with Pleo is to seamlessly export accounting entries.
To know more in detail, refer [here](/reference/export-api-overview).
## Customer Documentation
Once the integration is set up, you can develop some documentation for customers and provide the following assistance:
* How the customer can connect to the integration?
* Are there any best practices to follow?
* Troubleshooting problems
## Handling Errors
We have listed few commonly encountered errors that you might encounter while configuring the integration:
1. **Why are you encountering errors while accessing any Pleo APIs?**
Check whether you have the required scopes to access the relevant APIs.
2. **What to do if you cannot use the integration?**
Check the following:
* On the Pleo side — to use an ERP/accounting integration, you must either be an admin or an external bookkeeper with extended permissions.
* On the target application side - you might need require specific permissions to access the integration.
* You must have subscribed to the correct plan of the ERP/accounting system to use the Pleo integration; for example, Visma Economic has specific plans for their users to consume any external integration.
* Verify the data that you have entered. For example, an incorrect tax code could lead to an error.
3. **After the integration is connected, why are tags/tax codes/vendors not automatically synched between Pleo and the target ERP/accounting system?**
Check whether the integration supports any of these — if not, then tags/tax codes/vendors would not automatically sync.
# Terms of Service
Source: https://developers.pleo.io/page/terms-of-service
Pleo Technologies A/S’ API Terms of Service
By accessing or using Pleo Technologies A/S’ APIs (the “Service”), you are agreeing to the terms below. Collectively, we refer to the terms below together with the API documentation, and any applicable policies and guidelines as the "Terms."
You agree to comply with the Terms and that the Terms control your relationship with us.
Section 1: Account and Registration
1.1 Accepting the Terms
The Customer access to and use of the Service is conditioned on the acceptance of and compliance with these Terms and with the Terms of Condition, Terms of Service and Privacy Policy governing your use of other services offered by Pleo Technologies A/S and Pleo Financial Services A/S, respectively.
The duration of these Terms will be from the date upon which you agree to these Terms and will continue until terminated as set forth below.
1.2 Registration
In order to access the API the Customer may be required to provide certain information (such as identification or contact details) as part of the registration process for the APIs, or as part of the Customer continued use of the APIs.
Section 2: Using Our API
2.1 End User
The Customer (“End user”) shall be onboarded and fully verified as per described due diligence in Pleo Financial Services A/S’ Terms and Conditions. The Customer shall comply with applicable law, regulation, and the Terms.
2.2 Compliance with Law, Third Party Rights, and Other Agreements
The Customer will comply with all applicable law, regulation, and third party rights (including without limitation laws regarding the import or export of data or software, privacy, and local laws). The Customer will not use the API to encourage or promote illegal activity, violation of third party rights or violate any legal terms which govern the services offered by Pleo Technologies A/S and Pleo Financial Services A/S, respectively.
2.3 Permitted Access
By agreeing to these terms, the verified End User is granted API access to the corporate account. The Customer will only access the API by the means described in the documentation of that API.
2.4 API Limitations
Pleo Technologies A/S sets and enforces limits on your use of the APIs (e.g. limiting the number of API requests that you may make or the number of users you may serve), in our sole discretion. You agree to, and will not attempt to circumvent, such limitations documented with each API. If you would like to use any API beyond these limits, you must obtain Pleo Technologies A/S’s express consent (and Pleo Technologies A/Smay decline such request or condition acceptance on your agreement to additional terms and/or charges for that use). To seek such approval, contact [api@pleo.io](mailto:api@pleo.io)
2.5 Fees
Pleo reserves the right to implement fees and payment terms with respect to any use of the Pleo API.
2.6 Data Protection Terms
To the extent required by data protection laws applicable to the parties' processing personal data under these Terms, the parties agree on the following terms: Pleo Data Protection Agreement.
Section 3: Security
3.1 Monitoring
The APIs are designed to help the Customer enhance visibility. THE CUSTOMER AGREES THAT PLEO TECHNOLOGIES A/S MAY MONITOR USE OF THE API TO ENSURE QUALITY, IMPROVE PLEO TECHNOLOGIES A/S (AND ITS AFFILIATES’) PRODUCTS AND SERVICES, AND VERIFY YOUR COMPLIANCE WITH THE TERMS. Pleo Technologies A/S may suspend access to the APIs without notice if we reasonably believe that you are in violation of the Terms.
3.2 Security
Both parties will use commercially reasonable efforts to protect user information conveyed in the API, including personally identifiable information ("PII"), from unauthorized access or use and will promptly report to your users any unauthorized access or use of such information to the extent required by applicable law.
Section 4: Prohibitions and Confidentiality
4.1 API Prohibitions
When using the APIs, the Customer must not:
* Sublicense an API for use by a third party.
* Perform an action with the intent of introducing to \[]products and services any viruses, worms, defects, Trojan horses, malware, or any items of a destructive nature.
* Defame, abuse, harass, stalk, or threaten others.
* Interfere with or disrupt the API or the servers or networks providing the API.
* Promote or facilitate unlawful online activities or disruptive commercial messages or advertisements.
* Reverse engineer or attempt to extract the source code from any API or any related software.
4.2 Confidentiality
Developer credentials (such as passwords, keys, and client IDs) are intended to be used by the Customer and identify the Customer API access. The Customer will keep the credentials confidential and make reasonable efforts to protect them.
Section 5: Intellectual Property
The Terms do not grant either party any right, title, or interest in or to the other party's trade names, trademarks, service marks, logos, domain names, and other distinctive brand features.
The Terms do not create any third party beneficiary rights or any agency, partnership, or joint venture.
Section 6: Termination
6.1 Termination
The Customer may terminate these Terms by discontinuing use of Pleo API. API access will be revoked immediately upon termination of the business relationship with Pleo.
Pleo reserves the right to discontinue the API or any portion or feature or the access thereto for any reason and at any time without liability or other obligation to you. You are specially made aware that this is BETA version
6.2 Surviving Provisions
When the Terms come to an end, those terms that by their nature are intended to continue indefinitely will continue to apply.
Section 7: Liability for our APIs
7.1 Warranties
THE API IS A BETA VERSION AND IS STILL BEING DEVELOPED, TESTED AND EVALUATED. PLEO PROVIDES THE APIS "AS IS". PLEO DOES NOT MAKE ANY SPECIFIC PROMISES ABOUT THE API. PLEO DOES NOT MAKE ANY COMMITMENTS ABOUT THE CONTENT ACCESSED THROUGH THE API, THE SPECIFIC FUNCTIONS OF THE API, OR ITS RELIABILITY, AVAILABILITY, OR ABILITY TO MEET YOUR NEEDS.
7.2 Limitation of liability
To the extent permitted by law, Pleo are not liable for failure or delay in performance to the extent caused by circumstances beyond our reasonable control.
Section 8: General Provisions
8.1 Modification
We may modify the Terms or any portion to, for example, reflect changes to the law or changes to our API. You should look at the Terms regularly. We'll post notice of modifications to the Terms within the API documentation. Changes will not apply retroactively and will become effective no sooner than 30 days after they are posted. If you do not agree to the modified Terms for an API, you should discontinue your use of that API. Your continued use of the API constitutes your acceptance of the modified Terms.
# Overview
Source: https://developers.pleo.io/reference/accounting-entries/accounting-entry-overview
The Accounting Entries API endpoints help you retrieve accounting entries and receipts recorded in Pleo. You can apply filters to narrow your search, then export the results to third-party accounting systems and budget management tools.
An accounting entry is a record that provides details of a transaction, representing a single event that has either occurred or is scheduled to occur.
A few examples of accounting entries recorded in Pleo:
* Employees of a company paying with their Pleo card.
* Employees receiving reimbursement from the Pleo wallets for an expense incurred.
* An amount charged to the company for Pleo's monthly Software as a Service (SaaS) fees.
* A cashback amount debited to the company account from Pleo.
Accounting entries are central to the Pleo platform.
Accounting Entries were previously called `Expenses` in Pleo. As the platform expanded beyond expense tracking, the name was updated to reflect the broader range of transaction types now supported. You may still see the term `Expenses` in Legacy APIs.
If you're migrating from the [Expenses API](/reference/pleo-deprecated/expenses/get-an-expense-for-a-company), there are two pathways available depending on your use case. See the [migration guide](/deprecation/legacy-expense-api/overview) for details.
When accounting entries are created or updated, it may take a short time for those changes to appear in API responses.
## Tax
Tax is a critical part of any accounting entry, as it directly affects how transactions are recorded and reported. Tax calculation depends on the tax code rate and type, determined as follows:
* If `taxCodeId` is set, the rate and type are taken from the `TaxCode`.
* If `accountId` is set, the rate and type are taken from the account's `TaxCode`.
* If the tax code type is `NORMAL`, `grossBillValue` is the amount before tax and `netBillValue` is the amount including tax.
* If the tax code type is `REVERSE`, `grossBillValue` is the amount including tax and `netBillValue` is the amount excluding tax.
* If the accounting entry is of the family `MILEAGE`, tax is calculated differently based on geographic location:
* By default, no `TaxCode` is selected and the government statutory rule applies.
* This is overridden when a `TaxCode` is set.
* In some countries where commodity prices are government-advised, the tax reflects the advisory amount when no `TaxCode` is set.
# Scopes
Source: https://developers.pleo.io/reference/accounting-entries/scopes
The following table specifies the scopes required to access the Accounting Entries API endpoints:
| Operation | Scopes |
| :--------------------------------------- | :---------------------- |
| Search Accounting Entries | accounting-entries:read |
| Search Receipts | accounting-entries:read |
| Get Accounting Entry | accounting-entries:read |
| Get All Receipts of Accounting Entry | accounting-entries:read |
| Get Specific Receipt of Accounting Entry | accounting-entries:read |
# Fetch a single accounting entry by id.
Source: https://developers.pleo.io/reference/accounting-entries/v1/get-accounting-entry-by-id
get /v1/accounting-entries/{accountingEntryId}
# Get all receipts of an accounting entry.
Source: https://developers.pleo.io/reference/accounting-entries/v1/get-all-receipts
get /v1/accounting-entries/{accountingEntryId}/receipts
# Returns a receipt with time-bound access to the receipt file.
Source: https://developers.pleo.io/reference/accounting-entries/v1/get-receipt-file
get /v1/accounting-entries/{accountingEntryId}/receipts/{receiptId}
# Fetch accounting entries based on filters.
Source: https://developers.pleo.io/reference/accounting-entries/v1/search-accounting-entries
post /v1/accounting-entries:search
# Apply filters to search for specific accounting entry receipts.
Source: https://developers.pleo.io/reference/accounting-entries/v1/search-receipts-by-post
post /v1/accounting-entries/receipts:search
# Chart of Accounts API Overview
Source: https://developers.pleo.io/reference/accounts/chart-of-accounts-api-overview
# Overview
The Chart of Accounts API enables you to manage the chart of accounts recorded in Pleo.
## What is an Account?
An account represents a type of account in the chart of accounts; the chart of accounts is the customer's index of all financial accounts maintained in an ERP/accounting system. Hence, an account enables a customer to accurately bookkeep their accounting entries in the correct account in their ERP/accounting system.
## How is an Account Associated with a Category?
A category explains the purpose for which an expense has been incurred. For example, a hotel accommodation is booked for a company event using the Pleo card; hence, the **Travel > Accommodation (Hotel, Airbnb)** category is assigned to this expense.
For the purpose of bookkeeping, categories are mapped with an account in the chart of accounts. Assignment of categories help in allocating the expense to the correct account in the ERP/accounting system.
The “source of truth” for accounts lies within the target system, with Pleo just storing a cached version of the entity.
## Account Management
The Chart of Accounts API allows you to perform various operations on accounts, such as creating, retrieving, updating, and deleting accounts. Each account is identified by a unique `accountId`.
* An account exists in the external ERP/accounting system, but not in Pleo: the integration sends a request to the `POST Create a new account` endpoint to create the corresponding record in Pleo.
* Details of an account have been updated in the ERP/accounting system: the integration sends a request to the `PUT Update an account` endpoint to modify the details. Also, when an account is linked with accounting entries, it cannot be deleted. It can only be archived through this endpoint.
* Search for an account by its account ID: the integration must send a request to the `GET Retrieve an account in Pleo by its ID` endpoint.
* An account exists in Pleo, but not in the external ERP/accounting system: the integration sends a request to the `DELETE Delete an account` endpoint.
Ensure that when an account is linked to expenses and the accounting entries have been exported to the ERP/accounting system, the account cannot be deleted in Pleo.
* Apply specific filters to retrieve a list of matching accounts: the integration sends a request to the `POST Fetch a list of accounts` endpoint.
## Rate Limits
The APIs have a rate limit of 600 requests per minute.
# Chart of Accounts API Scopes
Source: https://developers.pleo.io/reference/accounts/chart-of-accounts-api-scopes
The following table specifies the scopes required to access the Chart of Accounts API endpoints:
| Operation | Scopes |
| :------------------------------------- | :------------- |
| Create/Update/Delete an account | accounts:write |
| Fetch an account or a list of accounts | accounts:read |
You can use an access token/API key for an organisation or a company. However, for organisations with a multi-entity set up, the chart of accounts is mapped between each entity (company) and Pleo.
# Create a new account
Source: https://developers.pleo.io/reference/accounts/create-a-new-account
post /v1/chart-of-accounts
The integration sends a request to this endpoint to create a corresponding account in Pleo for any new record created in the external ERP/accounting system.
The request will fail if an account with the same `externalId` and `companyId` already exists in Pleo.
# Create multiple accounts in a single request
Source: https://developers.pleo.io/reference/accounts/create-multiple-accounts-in-a-single-request
post /v1/chart-of-accounts/batch
Creates multiple bookkeeping accounts in a single batch operation.
This endpoint validates each account in the batch and returns both successfully created accounts and failed items with their failure reasons.
Accounts that pass validation will be created, while invalid accounts will be returned in the failed list.
# Delete an account
Source: https://developers.pleo.io/reference/accounts/delete-an-account
delete /v1/chart-of-accounts/{accountId}
Deletes an existing account in Pleo.
Only accounts not linked to any accounting entries can be deleted. Trying to delete an account linked to accounting entries will result in an error. If an account is linked, it can only be archived. Use UPDATE endpoint to archive an account.
# Fetch a list of accounts
Source: https://developers.pleo.io/reference/accounts/fetch-a-list-of-accounts
post /v1/chart-of-accounts:search
Retrieves a list of accounts with companyId and other optional filters. Results are paginated.
# Retrieve an account in Pleo by its ID
Source: https://developers.pleo.io/reference/accounts/retrieve-an-account-in-pleo-by-its-id
get /v1/chart-of-accounts/{accountId}
Search for a specific account by its ID.
# Update an account
Source: https://developers.pleo.io/reference/accounts/update-an-account
put /v1/chart-of-accounts/{accountId}
Update details of an existing account in Pleo.
The integration must send a request to this endpoint when it is trying to archive an account linked to accounting entries.
# Introduction
Source: https://developers.pleo.io/reference/api-introduction
Explore the API Reference tab to learn more about the Pleo APIs.
Each API is provided with a contextual explanation, scopes required, and detailed reference information about each endpoint.
We are developing new APIs. You will note that [this set of endpoints](/reference/notice) has been marked as **Deprecated**, implying that no new features will be added. Access will be completely revoked during 2026. Read more about the details and migration options [here](/reference/notice).
# API Response Codes | Errors
Source: https://developers.pleo.io/reference/api-response-codes-errors-1
Pleo uses HTTP status codes to indicate the success and failure of API requests.
In general, codes in the`2xx` range indicate `Success`; the request is successfully received, understood, and accepted. Whereas, codes in the `4xx` range indicate a client error — the request contains bad syntax or cannot be fulfilled. Errors in the `5xx` indicate a server error — the server failed to fulfil an apparently valid request. For generic knowledge on the status code, see this [topic](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/).
Here is a generic list of error codes and what they imply in the context of Pleo:
| Code | Error Type | Definition/Reasons | Resolution |
| :--- | :-------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400 | Bad Request | The server could not understand the request due to invalid syntax. | This error occurs when required information is missing or there is a validation error. Hence, review your request and ensure that you have entered all the mandatory data and the information is in the correct format. |
| 401 | Unauthorized | The client's request is unauthorised. This may occur for any of the following reasons: • The JWT token has expired • The token that you have used is in the wrong format • You are using an incorrect token • You have entered any token in the authorisation header | • You must get a new JWT token for accessing the Export API resources • Verify the token format • Use the correct refresh token • Check whether you have entered the token in the authorisation header • If the token has expired, initiate a new authorisation request • Review the token format • Ensure that you are using the correct refresh token and it has not expired • Check whether you have entered the correct token in the authorisation header |
| 403 | Forbidden | The client does not have enough access to perform the intended action. | Contact your developer partner manager for providing you access. |
| 404 | Not Found | The server cannot find the requested resource. | Review your request — either you have entered incorrect information or the resource does not exist. |
| 500 | Internal Server Error | The server has encountered a situation that it does not know how to handle. | Retry your request after some time. |
| 502 | Bad Gateway | The server, while acting as a gateway or proxy, received an invalid response from the upstream server. | Retry your request after some time. |
| 503 | Service Unavailable | The server is not ready to handle the request, primarily due to being overloaded or the server is under maintenance. | Retry your request after some time. |
# Attribute explanations
Source: https://developers.pleo.io/reference/attribute-explanations
This page refers to the Legacy API which is deprecated
Here you can find the attribute keys from all endpoints explained
| Key | Type | Description |
| :------------------------------ | :-------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id | *String* | The unique identifier of the requested item. |
| employeeId | *String* | The unique identifier of the Pleo user. |
| departmentId | *String* | The unique identifier of the Pleo team. |
| performedAt | date, ISO-8601 yyyy-mm-ddTHH:mi:ss.SSSZ | |
| amountOriginal: value | number | The value of the expense. |
| amountOriginal: currency | *String* | The currency of the expense. |
| note | *String* | Note included in Pleo for the expense. |
| type | *String* | Type of the expense. CARD, PERSONAL\_TRANSFER, BILL\_INVOICE, LOAD, MANUAL, MANUAL\_TRANSFER, PLEO\_INVOICE |
| cardTransaction: state | *String* | State of the transaction AUTHORIZATION, AUTHORIZATION\_REVERSAL, CHARGE\_BACK, CHARGE\_BACK\_REVERSAL, PRESENTMENT, SECOND\_PRESENTMENT, REFUND, LOAD, UNLOAD, FAILED |
| cardTransaction: authorisedAT | yyyy-mm-ddTHH:mi:ss.SSSZ | |
| cardTransaction: settledAt | yyyy-mm-ddTHH:mi:ss.SSSZ | |
| cardTransaction: reversedAt | yyyy-mm-ddTHH:mi:ss.SSSZ | |
| cardTransaction: merchant: name | *String* | |
| accountId | *String* | The unique identifier of the account (category) attached to the expense |
| tagIds | *String* | The unique identifiers of the tags attached to the expense |
| receiptIds | *String* | The unique identifiers of the receipts attached to the expense |
| status | *String* | Export state of the expense as reflected in Pleo. NOT\_EXPORTED, QUEUED, EXPORTING, EXPORTED |
| createdAt | yyyy-mm-ddTHH:mi:ss.SSSZ | Date format specifying when the expense was created |
# Authentication
Source: https://developers.pleo.io/reference/authentication
# Authentication for the Legacy API
Source: https://developers.pleo.io/reference/authentication-legacy
All REST API resources are authenticated with HTTP Bearer access tokens. You can manage access tokens on your company settings page.
This page refers to the Legacy API which is deprecated
## API Keys
The Pleo API uses API keys to authenticate requests. You can view and manage your API keys in the Pleo UI in "Settings"
Keep your key safe! Your API keys carry many privileges, so be sure to keep them secure! Do not share your secret API keys in publicly accessible areas such as GitHub, client-side code, and so forth.
Create your individual **Open API Token** with the steps below:
1. Open the Pleo web app and go to your Settings
2. Click on Open API
3. Click on Create token in the top right corner
## Independent Software Vendors
Please ensure that you have an active Pleo **admin** account.
If you are integrating on behalf of your customer, please contact us at [api@pleo.io](mailto:api@pleo.io)!
# Company API Overview
Source: https://developers.pleo.io/reference/companies/companies-api-overview
# Overview
The Companies API enables you to perform the following:
* Search by organisation ID: Returns the list of companies associated with the organisation.
* Search by company ID: Returns details of the specific company.
## Rate Limits
The API has a rate limit of 600 requests per minute.
# Scopes
Source: https://developers.pleo.io/reference/companies/companies-api-scopes
The following table specifies the scope required to access the Companies API endpoints:
| Operation | Scopes |
| :------------------- | :------------- |
| Search for Companies | companies:read |
| Get Company by ID | companies:read |
# Search for a Specific Company
Source: https://developers.pleo.io/reference/companies/search-for-a-specific-company
get /v1/companies/{companyId}
Search for a specific company by company ID.
# Search for Companies
Source: https://developers.pleo.io/reference/companies/search-for-companies
get /v1/companies
Use this endpoint to search for companies associated with a specific organisation.
# Overview
Source: https://developers.pleo.io/reference/employees/employees-api-overview
The Employees API enables you to perform the following:
* Searching for employees by company ID, email, or code that refers to the employees' external IDs as maintained in the target ERP/accounting system.
* Fetching an employee details by ID.
# Rate Limits
The APIs have a rate limit of 600 requests per minute.
# Scopes
Source: https://developers.pleo.io/reference/employees/employees-api-scopes
The following table specifies the scopes required to access the Employee API endpoints:
| Operation | Scopes |
| :----------------- | :--------- |
| Search Employees | users:read |
| Get Employee by ID | users:read |
You can search for employees both at the organisation and the company level.
# Search for a specific employee
Source: https://developers.pleo.io/reference/employees/search-for-a-specific-employee
get /v2/employees/{employeeId}
Search for a specific employee by employee ID.
# Search for employees
Source: https://developers.pleo.io/reference/employees/search-for-employees
get /v2/employees
Use this endpoint to search for multiple employees.
# Overview
Source: https://developers.pleo.io/reference/enrichment-api/enrichment-api-overview
The Accounting Enrichment API enables external partner systems to attach receipts to existing Pleo card expenses. Cost categorisations (Pleo Tags) can optionally be applied at the same time.
## Key Concepts
### What is an Accounting Entry?
An accounting entry is Pleo's representation of a spend transaction enriched with details such as receipts, tags, and notes.
### What is the Receipt Inbox?
The Receipt Inbox is the spender's inbox in the Pleo mobile app. Receipts that could not be matched to an accounting entry are placed there for the spender to manually match.
## Authentication
Authentication uses [Pleo's OAuth flow](/docs/current/integration-design/auth/oauth/integration-design-auth-oauth-overview) or [Standalone API Keys](/docs/current/how-tos/environment-access/how-to-get-access-to-all-env-standalone-api-keys), with the `accounting-entries:write` scope.
You can authenticate as an organisation or a company. For organisations with a multi-entity setup, you must use a company-level token (or API key) to ensure enrichment requests target the correct company's accounting entries.
## How It Works
The integration sends an enrichment request to Pleo containing transaction details and enrichment data. Pleo uses the transaction details to identify the matching accounting entry and enriches it with the provided receipt and, optionally, tags.
See the [Enrichment Workflow Quick Guide](/reference/enrichment-api/enrichment-workflow-basic-guide) to get started.
## Request Parameters
### Transaction Details
Transaction details are used by Pleo to identify the correct accounting entry. All details are passed as query parameters.
| Parameter | Required | Description |
| ----------------------- | -------- | ---------------------------------------------------- |
| `amount` | Yes | Transaction amount in minor units (e.g. cents) |
| `currency` | Yes | ISO 4217 currency code |
| `card_last_four_digits` | Yes | Last four digits of the Pleo card used |
| `merchant_name` | Yes | Name of the merchant |
| `performed_at` | No | Timestamp of transaction authorisation |
| `settled_at` | No | Timestamp of settlement |
| `email` | No | Email address of the Pleo user who made the purchase |
Provide at least one date parameter (`performed_at` or `settled_at`). The more details provided, the higher the likelihood of a successful match.
### Enrichment Data
Enrichment data is passed in the request body.
| Field | Required | Description |
| ---------- | -------- | --------------------------------------------------------------------------------- |
| `receipts` | Yes | One or more Base64-encoded receipt files. Supported formats: PDF, PNG, JPEG, HEIC |
| `tags` | No | List of Pleo Tag UUIDs. See [Tags API](/reference/tags-overview#tags-object) |
## Receipt Statuses
Pleo returns a receipt status with every enrichment response.
| Status | HTTP | Description |
| ------------------------ | ---- | -------------------------------------------------------------------------------- |
| `UPLOADED` | 201 | Match found; receipt attached to the accounting entry |
| `ACCEPTED` | 202 | Request accepted; Pleo will retry matching asynchronously |
| `ADDED_TO_RECEIPT_INBOX` | 200 | No match found; receipt stored in the Receipt Inbox |
| `UNRESOLVED` | 404 | Request could not be processed; receipt was not attached to any accounting entry |
## Supported Features
v2 supports:
* Multiple receipts per request
* Asynchronous matching via the `ACCEPTED` status
# Scopes
Source: https://developers.pleo.io/reference/enrichment-api/enrichment-api-scopes
The following table specifies the scopes required to use the Accounting Enrichment API.
| Operation | Scope |
| ----------------------- | -------------------------- |
| Enrich Accounting Entry | `accounting-entries:write` |
You can authenticate as an organisation or a company. For organisations with a multi-entity setup, you must use a company-level token (or API key) to ensure enrichment requests target the correct company's accounting entries.
# Enrich Accounting Entry
Source: https://developers.pleo.io/reference/enrichment-api/enrichment-api-v2/enrich-accounting-entry
post /v2/accounting-entries:enrich
Enriches an existing Pleo card accounting entry by attaching one or more receipts and applying optional cost categorisation tags. Pleo identifies the matching entry using the transaction details provided as query parameters. Use this endpoint for all new implementations.
# Enrichment Workflow Quick Guide
Source: https://developers.pleo.io/reference/enrichment-api/enrichment-workflow-basic-guide
A quick reference to the key steps for implementing the Accounting Enrichment workflow: build the payload, submit the request, handle responses, and optionally apply tags.
This quick guide walks through the steps to implement Accounting Enrichment. For parameter definitions, status descriptions, and API versions, see the [Enrichment API Overview](/reference/enrichment-api/enrichment-api-overview).
This is a one-way flow from your integration into Pleo. Before you start, your integration needs to already have the receipt file, the card's last four digits, and any other matching information the endpoint supports. Pleo then attempts to match the submitted details to an existing accounting entry. The endpoint is a POST, not a GET: it enriches an existing accounting entry with what you send it, and it can't be used to retrieve a receipt file or the card's last four digits from Pleo.
## Prerequisites
Before starting this workflow, your integration must already have:
* Authentication with the `accounting-entries:write` scope. See [Accounting Enrichment API Scopes](/reference/enrichment-api/enrichment-api-scopes).
* The receipt file to upload, in a supported format (PDF, PNG, JPEG, HEIC).
* The last four digits of the Pleo card used for the transaction.
* The required transaction details for matching: `amount`, `currency`, `merchant_name`, and at least one of `performed_at` or `settled_at` (optionally `email`). See [Transaction Details](/reference/enrichment-api/enrichment-api-overview#request-parameters).
## Workflow Overview
| Step | What happens |
| -------------------------------- | ----------------------------------------------------------- |
| 1. Build the enrichment payload | Gather transaction details and encode the receipt as Base64 |
| 2. Submit the enrichment request | Call the v2 enrichment endpoint |
| 3. Handle the response | Act on the receipt status returned by Pleo |
## Step 1: Build the Enrichment Payload
Using the receipt and transaction details already available from prerequisites, encode the receipt file as Base64.
For the full list of required and optional parameters, see [Request Parameters](/reference/enrichment-api/enrichment-api-overview#request-parameters).
The encoded string is submitted in the `receipts` array in the request body.
## Step 2: Submit the Enrichment Request
**Endpoint:** POST [`/v2/accounting-entries:enrich`](/reference/enrichment-api/enrichment-api-v2/enrich-accounting-entry)
Submit transaction details as query parameters and the encoded receipt in the request body:
```json theme={null}
{
"receipts": [""]
}
```
## Step 3: Handle the Response
Pleo returns a `receiptStatus` with every response. For status definitions, see [Receipt Statuses](/reference/enrichment-api/enrichment-api-overview#receipt-statuses).
* **`UPLOADED`**: Enrichment is complete. Store the `accountingEntryId` for reference.
* **`ACCEPTED`**: Pleo is retrying asynchronously. No action required; retrying will create duplicate receipts.
* **`ADDED_TO_RECEIPT_INBOX`**: Pleo could not make a reliable match. The receipt is placed in the spender's Pleo Receipt Inbox, accessible on mobile, for manual matching. Retry if within your retry window, otherwise stop.
* **`UNRESOLVED`**: The request could not be processed. Investigate the transaction details and do not retry with the same payload.
## Applying Tags (Optional)
Tags apply Accounting Dimensions (Pleo Tags) to the matched accounting entry at enrichment time. For full details on how tags work, see [Tags Sync](/docs/current/integration-design/accounting-integrations/imports/tags/integration-design-tags-overview). Include validated Pleo Tag UUIDs in the request body alongside the receipt:
```json theme={null}
{
"receipts": [""],
"tags": [""]
}
```
Before submitting with tags:
1. Retrieve tag IDs from your Tags Sync implementation: do not use hardcoded IDs.
2. Confirm each tag ID is present and not archived in your most recent synced snapshot.
3. If any ID cannot be validated, re-fetch from the Pleo Tags API and remap where possible.
4. If valid IDs cannot be resolved, omit the `tags` field and submit the receipt only.
Never submit with invalid tag IDs. Pleo validates all submitted tag IDs before processing: if any ID is invalid, the entire request is rejected with `UNRESOLVED` and the receipt will not be attached to any accounting entry.
If Pleo rejects a tag, re-fetch tags, remap where possible, and resubmit. If valid IDs cannot be resolved, resubmit without the `tags` field.
# Errors
Source: https://developers.pleo.io/reference/errors
This page refers to the Legacy API which is deprecated
## Error Handling & Responses
Pleo uses conventional HTTP response codes to indicate the success or failure of an API request. In general: Codes in the 2xx range indicate success. Codes in the 4xx range indicate an error that failed given the information provided (e.g., a required parameter was omitted, a charge failed, etc.). Codes in the 5xx range indicate an error with Pleo's servers (these are rare).
Some 4xx errors that could be handled programmatically - include an error code that briefly explains the error reported.
| Error code | Description of error code |
| :--------- | :-------------------------------------------------------- |
| 200 | All good, everything went as expected! |
| 401 | Unauthorised - No valid Token has been provided |
| 403 | Forbidden - The Token does not have sufficient permission |
| 404 | The requested resource does not exist |
| 500 | Internal server error |
# Export API Overview
Source: https://developers.pleo.io/reference/export-api/export-api-overview
Please note the following:
* V0 would be deprecated in 2025. Closer to the deprecation date, we would inform you.
* V1 is operational, but we recommend you to upgrade to V2 — the user attribute in `GET Export Items` response would be null, if there is no user/employee associated with the export item.
# Overview
The Export API enables you to export the accounting data managed in Pleo to other external ERP/accounting systems.
The target audience of this API is developer partners and customers who want to export the spend data managed in Pleo to an external ERP/accounting system of their choice.
Some of the key features and benefits of this API include:
* The accounting and the bookkeeping information contained in the accounting entries are transformed to exportable data items and packaged as **export items**. This is done to ensure accurate and transparent transmission of accounting entries from Pleo to the external ERP/accounting system.
* Managing the export workflow with `Export Jobs` and `Export Job Events` (for more information, see #What are Export Jobs? and #What are Export Job Events?).
* Tracking the progress of each accounting entry included in an export job through **export job items** — an **export job item** tracks the export progress of a specific accounting entry in an export job.
## What are Export Jobs?
An `Export Job` is the task that manages the export workflow and provides insight with the additional information:
* Status and progress of the export workflow
* Export history logs
* Orchestrates the entire workflow, like locking the accounting entries being exported from any modification
* Orchestrates the change of status in expenses
## What are Export Job Events?
The **Export Job Events** cause change in status of an export job. When an event is triggered, the status of the corresponding export job is impacted. This is returned in the `status` parameter of the `export-jobs` endpoint. For example, a specific export job is showing `failed` status; the reason of failure is also mentioned to understand what caused the problem and how it could be resolved.
The possible values are:
* `pending`: The accounting entries are queued for export.
* `failed`: The accounting entries could not be exported to the external accounting system.
* `completed`: The accounting entries have been successfully exported to the external accounting system.
* `completed_with_errors`: The export workflow is complete, yet few accounting entries could not be exported to the external accounting system.
## Webhook Notifications for Status Change in Export Workflow
When an **Export Job Event** is triggered, a webhook notification is generated and sent to the external accounting system. Thus, this helps the ERP/accounting system to act in accordance with the event status. For example, if you have subscribed to the `v2.export.job-created` event, a webhook notification is sent to the external system, notifying that an export workflow is already initiated and a specific set of accounting entries are being currently exported from Pleo to the target system. For more information, see [Webhooks](/reference/overview-webhooks).
If webhook implementation is unavailable for any export job event, you might poll `GET https://external.pleo.io/v2/export-jobs` to receive additional information.
## Vendor Tagging in Export Items
Adding vendors in an export item is a new functionality that is available only when the following criteria are met:
* The specific company has synchronised the list of vendors existing in their ERP/accounting system and their Pleo account. For more information, see [Vendors API Overview](https://developers.pleo.io/reference/vendors-api-overview).
* The integration supports the capability of vendor tagging. If yes, `vendorBasedBookkeeping` is set to `True` in the `POST Create a new export job` endpoint.
* Tagging vendors in an export item is available for the following expenses:
* `card_purchase`
* `fee`
* `chargeback`
* `refund`
* `cashback`
* `invoice`
* `invoice_payment`
* `overdraft`
Based on what the bookkeeping method is, tagging vendors in an export item is either **mandatory** or **optional**:
* If the bookkeeping method is `accounts_payable`, tagging vendors in an export item is **mandatory** — this helps to bookkeep the expense in the correct vendor account as maintained in the external ERP/accounting system. If `vendorBasedBookkeeping` is enabled, the `GET Get Export Items` endpoint includes vendor and bookkeeping data points.
* If the bookkeeping method is `journal`, tagging vendors in an export item is **optional**.
i. If there is any problem in bookkeeping the export item to the correct vendor account (like, when the vendor `code` is not matching) in the ERP/accounting system, the integration must send a request to the `PUT Update Export Job Items` endpoint with `failureReason` specifying that vendor record could not be identified and `failureReasonType` set to `accounting_system_validation_failure`.
ii. In the Export API Version 2, you might find the `supplier` object, instead of `vendor`. To bookkeep the export item using the `supplier` object, ensure that any of the following information is present to find the appropriate vendor in the ERP/accounting system:
```
- `code` matching the supplier ID as recorded in the accounting system
- `account` matching the supplier account number as recorded in the accounting system
- `taxIdentifier` matching the tax identifier of the supplier as recorded in the accounting system
```
## Rate Limits
The APIs have a rate limit of 600 requests per minute.
# Export API Scopes
Source: https://developers.pleo.io/reference/export-api/export-api-scopes
The following table specifies the scopes required to access the Export API endpoints:
| Operation | Scopes |
| :------------------------- | :---------------- |
| Create an Export Job Event | export-jobs:write |
| Get a list of Export Jobs | export-jobs:read |
| Create a new export job | export-jobs:write |
| Get an Export Job | export-jobs:read |
| Get Export Job Items | export-jobs:read |
| Update Export Job Items | export-jobs:write |
| Get Export Items | export-items:read |
You can use an access token/API key of an organisation or a company. However, for an organisation with a multi-entity set up, you must use the `company_id` in the request parameters as you can export accounting entries from Pleo for a specific company only.
# Create a new export job
Source: https://developers.pleo.io/reference/export-api/export-api-v1/create-a-new-export-job
post /v1/export-jobs
# Create an Export Job Event
Source: https://developers.pleo.io/reference/export-api/export-api-v1/create-an-export-job-event
post /v1/export-job-events
The Export Job Events imply the different status of an Export Job through the export lifecycle.
Use this endpoint to create a new status.
For example, started event implies the export job is initiated.
The existing Export Job Events are as follows:
- started: An export job is initiated. This allows the integration
service to start processing the export job items.
- failed: The export job has failed when an irrecoverable error happens and this could have happened while processing the export job items.
- completed: The export job items have been successfully exported.
- completed_with_errors: The export job items have been processed, although some of them could not be exported.
# Get a list of Export Jobs
Source: https://developers.pleo.io/reference/export-api/export-api-v1/get-a-list-of-export-jobs
get /v1/export-jobs
Fetch a list of export jobs
# Get an Export Job
Source: https://developers.pleo.io/reference/export-api/export-api-v1/get-an-export-job
get /v1/export-jobs/{jobId}
Retrieve an export job for a given export job ID
# Get Export Job Items
Source: https://developers.pleo.io/reference/export-api/export-api-v1/get-export-job-items
get /v1/export-jobs/{jobId}/items
Fetch a list of export job items
# Update Export Job Items
Source: https://developers.pleo.io/reference/export-api/export-api-v1/update-export-job-items
put /v1/export-jobs/{jobId}/items
Update the status and other attributes of the export job items in batches of 100. This API only supports updating up to 100 items.
# Create a new export job
Source: https://developers.pleo.io/reference/export-api/export-api-v2/create-a-new-export-job
post /v2/export-jobs
Use this endpoint to create a new export job that would transfer a list of specific accounting entries from Pleo to the target ERP/accounting system.
# Create an Export Job Event
Source: https://developers.pleo.io/reference/export-api/export-api-v2/create-an-export-job-event
post /v2/export-job-events
The Export Job Events imply the current status of the Export Job through the export lifecycle.
Send a request to this endpoint to update the status of the export job. If an export job has failed,
mention the failure reason and the reason type.
# Get a list of Export Jobs
Source: https://developers.pleo.io/reference/export-api/export-api-v2/get-a-list-of-export-jobs
get /v2/export-jobs
Fetch a list of export jobs.
# Get an Export Job
Source: https://developers.pleo.io/reference/export-api/export-api-v2/get-an-export-job
get /v2/export-jobs/{jobId}
Retrieve an export job by a specific export job ID.
# Get Export Job Items
Source: https://developers.pleo.io/reference/export-api/export-api-v2/get-export-job-items
get /v2/export-jobs/{jobId}/items
An export job item tracks the export progress of a specific accounting entry in an export job.
Use this endpoint to retrieve a list of export job items that you want to track.
# Update Export Job Items
Source: https://developers.pleo.io/reference/export-api/export-api-v2/update-export-job-items
put /v2/export-jobs/{jobId}/items
Update the status and other attributes of the export job items in batches of 100. This API only supports updating up to 100 items.
# Create a new export job
Source: https://developers.pleo.io/reference/export-api/export-api-v3/create-a-new-export-job
post /v3/export-jobs
Use this endpoint to create a new export job that would transfer a list of specific accounting entries from Pleo to the target ERP/accounting system.
# Create an Export Job Event
Source: https://developers.pleo.io/reference/export-api/export-api-v3/create-an-export-job-event
post /v3/export-job-events
The Export Job Events imply the current status of the Export Job through the export lifecycle.
Send a request to this endpoint to update the status of the export job. If an export job has failed,
mention the failure reason and the reason type.
# Get a list of Export Jobs
Source: https://developers.pleo.io/reference/export-api/export-api-v3/get-a-list-of-export-jobs
get /v3/export-jobs
Fetch a list of export jobs.
# Get an Export Job
Source: https://developers.pleo.io/reference/export-api/export-api-v3/get-an-export-job
get /v3/export-jobs/{jobId}
Retrieve an export job by a specific export job ID.
# Get Export Job Items
Source: https://developers.pleo.io/reference/export-api/export-api-v3/get-export-job-items
get /v3/export-jobs/{jobId}/items
An export job item tracks the export progress of a specific accounting entry in an export job. Use this endpoint to retrieve a list of export job items that you want to track.
# Update Export Job Items
Source: https://developers.pleo.io/reference/export-api/export-api-v3/update-export-job-items
put /v3/export-jobs/{jobId}/items
Update the status and other attributes of the export job items in batches of 100. This API only supports updating up to 100 items.
# Export API Workflow
Source: https://developers.pleo.io/reference/export-api/export-api-workflow
This topic explains the Export API workflow:
This workflow explains a usual course of export functionality and does not include exceptional scenarios.
1. A user selects specific accounting entries for export from Pleo to an external ERP/accounting system.
2. The accounting entries are transformed to exportable data items and packaged as **export items**.
3. At this stage, an export job is initiated in the `pending` status.
4. If the external ERP/accounting system sends a request to the `GET Get a list of Export Jobs` with `company_id` and `pending` status in the query parameters, the endpoint response sends a list of export jobs awaiting in the `pending` status; the endpoint response also contains the corresponding export job IDs. If no export job is available in the `pending` status, no response is sent.
5. The export job now begins to transfer the **export items** from Pleo to the ERP/accounting system.
6. Simultaneously, the external ERP/accounting system sends a request to the `POST Create Export Job Event` endpoint with `started` as the value for `event` parameter.
This changes the state of the `pending` export job to `in_progress` state.
7. At the end of the export, the export job could have either of the following status:
* `failed`: The export items could not be transferred to the ERP/accounting system. If the export job has `failed`, `failureReason` and `failureReasonType` are additional parameters that the ERP/accounting system specify while sending a request to the `POST Create Export Job Event` endpoint to update the status of the corresponding export job.
* `completed`: The export job is successfully completed and all the export items have been transferred to the ERP/accounting system.
* `completed_with_errors`: Some export items have been exported, while some could not.
If the export job has `completed` status, each accounting entry is assigned an `exportedAt`, `externalId`, and `externalUrl` - this information is returned in the `GET Get Export Job Items` endpoint response.
The Pleo application also accordingly updates the export workflow status:
* All successfully exported accounting entries are moved to the **Exported** tab.
* The accounting entries that could not be exported remain in the **Export queue**.
- If the external ERP/accounting system sends a request to the `GET Get Export Items` endpoint, specifying a certain export job ID, it returns a list of accounting entries that have been selected for export in a specific export job. The endpoint response informs you about the accounting entries that are selected for export in an export job.
- Send a request to the `PUT Update Export Job Items` to modify details of accounting entries that either have been successfully exported to the ERP/accounting system (for `successful` status), not yet exported (for `pending` and `in_progress` status), or not exported (for `failed` and `abandoned` status).
# Get Export Items
Source: https://developers.pleo.io/reference/export-api/export-items-v1/get-export-items
get /v1/export-items
Fetch a list of export items that have been marked for export for a given job. The data for these accounting entries are transformed accordingly for export purposes.
# Get Export Items
Source: https://developers.pleo.io/reference/export-api/export-items-v2/get-export-items
get /v2/export-items
An export item = an accounting entry that has been transformed into an exportable entry.
The required accounting and bookkeeping information is aggregated from an accounting entry to become an export item. Use this endpoint to retrieve a list of export items that have been selected for export. Please note that you can retrieve list of export items only from `pending` and `in_progress` export jobs.
# Get Export Items
Source: https://developers.pleo.io/reference/export-api/export-items-v3/get-export-items
get /v3/export-items
An export item = an accounting entry transformed into an exportable entry. The accounting and bookkeeping information is collected from an accounting entry to form an export item.
• A request to this endpoint retrieves a list of export items selected for export.
• Please note that you can retrieve a list of export items only from `pending` and `in_progress` export jobs.
# Multi Currency Configuration
Source: https://developers.pleo.io/reference/export-api/multi-currency-configuration
We provide multi-currency support, which enables you to maintain multiple accounts with Pleo in different currencies. The multi-currency configuration in the Pleo application helps our customers to optimise foreign exchange and simplifies operations when they are working with revenue and spend in multiple currencies.
## Multi-Currency in Export API
`contraAccount` object is returned in the endpoint response of `GET https://external.pleo.io/v3/export-items`, when the expense is any of the following types and it impacts the account balance:
* `card_purchase`
* `fee`
* `cashback`
* `refund`
* `chargeback`
* `direct_reimbursement`
* `invoice_payment`
* `card_invoice`
* `atm_withdrawal`
* `personal_spend`
* `wallet_topup`
* `wallet_unload`
## Frequently Asked Questions
No, the version 2 `GET Get Export Items` endpoint returns `400: Bad Request` error and displays the following:
```json JSON theme={null}
{
"type": "UNSUPPORTED_API_VERSION",
"message": "Upgrade to Export API v3 or higher: The API version you are trying to access does not support multi-currency wallets."
}
```
This ensures accurate bookkeeping, as when multi-currency configuration is enabled, the integration must access the Export API version 3 for the correct export of accounting entries from Pleo to the external ERP/accounting system.
Yes. Even if a customer has not activated multi-currency on Pleo, contra account, if configured, will be sent in V3 of `GET Get Export Items` endpoint. If contra accounts are not fully configured, then the endpoint will return status code `400` along with the following response body:
```json JSON theme={null}
{
"type": "MISSING_CONTRA_ACCOUNTS",
"message": "Make sure you don't export EUR, USD expenses before you set your contra accounts."
}
```
This message will be based on export item currencies for which there is no contra account set.
You can configure the integration to either fail the corresponding export job or wait until it expires.
An export job expires after one hour since its last update (`lastUpdatedAt`).
## Export API Version 3 URLs
Production: [https://external.pleo.io](https://external.pleo.io)
Staging: [https://external.staging.pleo.io](https://external.staging.pleo.io)
In the endpoint specific paths, use `v3` instead of `v2`. For example, `POST https://external.pleo.io/v3/export-job-events`.
# Vendor Tagging in Export Items
Source: https://developers.pleo.io/reference/export-api/vendor-tagging-in-export-items
You can add vendors in an export item, only when:
* The specific company has synchronised the list of vendors existing in their ERP/accounting system with their Pleo account. For more information, see [Vendors API](/reference/vendors-api-overview).
* The integration supports the capability of vendor tagging. If yes, `vendorBasedBookkeeping` is set to `True` in the `POST Create a new export job` endpoint.
## Vendor Tagging Enabled for Specific Expenses
You can tag vendors in an export item, when the expense belongs to any of the following types:
* `card_purchase`
* `fee`
* `chargeback`
* `refund`
* `cashback`
* `invoice`
* `invoice_payment`
* `overdraft`
## Vendor Tagging | Mandatory or Optional
Based on what the bookkeeping method is, tagging vendors in an export item is either **mandatory** or **optional**:
* If the bookkeeping method is `accounts_payable`, tagging vendors in an export item is **mandatory** — this helps to bookkeep the expense in the correct vendor account as maintained in the external ERP/accounting system. If `vendorBasedBookkeeping` is enabled, the `GET Get Export Items` endpoint includes vendor and bookkeeping data points.
* If the bookkeeping method is `journal`, tagging vendors in an export item is **optional**.
- If there is any problem in bookkeeping the export item to the correct vendor account (like, when the vendor `code` is not matching) in the ERP/accounting system, the integration must send a request to the `PUT Update Export Job Items` endpoint with `failureReason` specifying that vendor record could not be identified and `failureReasonType` set to `accounting_system_validation_failure`.
- In the Export API Version 2, you might find the `supplier` object, instead of `vendor`. To bookkeep the export item using the `supplier` object, ensure that any of the following information is present to find the appropriate vendor in the ERP/accounting system:
* `code` matching the supplier ID as recorded in the accounting system
* `account` matching the supplier account number as recorded in the accounting system
* `taxIdentifier` matching the tax identifier of the supplier as recorded in the accounting system
# Introspect the details of the current authentication.
Source: https://developers.pleo.io/reference/external-clients/introspect-the-details-of-the-current-authentication
get /v1/introspect
Returns the detail of the current authentication and what scope of the authentication (i.e API KEY or OAuth bearer token)
# Activate a Vendor
Source: https://developers.pleo.io/reference/external-vendors/activate-a-vendor
post /v1/vendors/{id}:activate
Updates the vendor state to Active, reflecting its availability in the target ERP/accounting system
# Archive a Vendor
Source: https://developers.pleo.io/reference/external-vendors/archive-a-vendor
post /v1/vendors/{id}:archive
Archives a Vendor in Pleo by its id.
# Create a New Vendor
Source: https://developers.pleo.io/reference/external-vendors/create-a-new-vendor
post /v1/vendors
This endpoint has the following functions:
- Create a corresponding record in Pleo for any new vendor created in the ERP/accounting system.
- Create a new vendor in Pleo, without the record yet existing in the ERP/accounting system.
# Fetches Vendors by search criteria
Source: https://developers.pleo.io/reference/external-vendors/fetches-vendors-by-search-criteria
post /v1/vendors:search
Retrieves a list of vendors with filters. Results are paginated.
# Retrieves a Vendor in Pleo by its id
Source: https://developers.pleo.io/reference/external-vendors/retrieves-a-vendor-in-pleo-by-its-id
get /v1/vendors/{id}
Retrieves a Vendor in Pleo by its id.
# Update a Vendor
Source: https://developers.pleo.io/reference/external-vendors/update-a-vendor
put /v1/vendors/{id}
Updates a Vendor for a company in Pleo.
# FAQ
Source: https://developers.pleo.io/reference/faq-1
Frequently asked questions on the Open API
This page refers to the Legacy API which is deprecated
## What do I need to get started?
* An active token
* A developer
## Where do I activate the token?
Make sure you are an admin on the Pleo account and go to your account Settings -> Open API and Create a token there.
## How do I integrate with my accounting system?
What we can help with here is to get all the necessary information out of Pleo but cannot assist with specific integration requests. You may want to contact the relevant accounting system to help you with the set up.
## What's an easy way to test an API call?
You can use the simple interface that we have in our documentation, as en example you can:
1. Select the GET expenses for a company
2. Enter any specific parameters you want to filter your result by
3. Enter your Bearer token
4. Click Try It!
You will see the results of your query in the response field.
## How do I use Postman?
You can make an easy call by copying and pasting the url from the documentation, for example a GET request [https://openapi.pleo.io/v1/expenses](https://openapi.pleo.io/v1/expenses).
Then enter your Authorisation Bearer token and click Send.
## What are the results of the API call?
You can see the correct results in the 200 response of each endpoint listed under Pleo.io API.
# Activate the installation for a client
Source: https://developers.pleo.io/reference/installations/activate-the-installation-for-a-client
post /v1/installations/me:activate
The integration sends a request to this endpoint to activate the installation, post authorisation. The request is sent after the integration has synchronised tags, tax codes, and vendors between the Pleo account and the client. Note that this is applicable when OAuth is used for authorisation.
# Complete the installation of a client
Source: https://developers.pleo.io/reference/installations/complete-the-installation-of-a-client
put /v1/installations/completions
**Deprecated**: Please use /installations/me:activate instead. Marks the installation as complete
# Create a client installation
Source: https://developers.pleo.io/reference/installations/create-a-client-installation
post /v1/installations/me
The integration sends a request to this endpoint to create an installation. This is required, especially when API keys are used for authorisation.
# Delete client's installation
Source: https://developers.pleo.io/reference/installations/delete-clients-installation
delete /v1/installations/me
Deletes the installation for the client and will revoke all access and refresh tokens for the client.
# Get client installation
Source: https://developers.pleo.io/reference/installations/get-client-installation
get /v1/installations/me
Retrieve the list of installations that you have done
# Update a client installation
Source: https://developers.pleo.io/reference/installations/update-a-client-installation
put /v1/installations/me
Send a request to this endpoint if you want to modify the installation status or the metadata.
# Migrating from /completions to /me:activate
Source: https://developers.pleo.io/reference/marketplace/installations-completions-migration
The `PUT /installations/completions` endpoint has been deprecated in favor of the `POST /installations/me:activate` endpoint. The new endpoint works exactly the same way as the old one, but it is more consistent with the other endpoints, and compliant with the Pleo API Standard.
## Migration Guide
1. Replace the `PUT /installations/completions` endpoint with the `POST /installations/me:activate` endpoint.
2. Ensure the OAuth access token is included in the request `Authorization: Bearer {token}` header.
3. Done
# Overview
Source: https://developers.pleo.io/reference/marketplace/marketplace-overview
Pleo as a spend management platform can be integrated with other third-party ERP/accounting applications or Human Resources Information Systems (HRIS) to make it more optimised for our customers. These applications make Pleo more dynamic and are useful in automating workflows and supporting additional functionalities.
All the applications that are available for integration with your Pleo account are listed in the App Marketplace.
## Installations
An installation is an integration between an application (client) and a Pleo account. When an application is integrated with Pleo for a user's account, an installation is created.
As an external developer, you can manage the integrations using the [Installations API](/reference/installations/get-client-installation). The API allows you to create, update, and delete installations, as well as retrieve information about them.
The installation has a `metadata` attribute that the application uses to store additional information about the installation. This can be useful for storing configuration settings or other data specific to the installation.
This `metadata` is in no way used or processed by Pleo, and is only stored for the application to use.
### Listing New Applications in the App Marketplace
If you are an external developer partner interested in listing a new application in the app marketplace, either you must contact your developer partner manager or submit this [form](https://forms.gle/ZzGPAs9vaZsTTibN6).
For new developer partners interested in collaborating with Pleo and accessing Pleo APIs, submit this [form](https://docs.google.com/forms/d/e/1FAIpQLSeQ7G_uzIzXPx-Iw5LLB5VC4IOCUuLoXXAaidBvE1IY72l9uw/viewform). For more information, read this [topic](/docs/build-with-pleo) to learn more.
### Creating Installations
Installations can be authorised either by OAuth or API Keys:
* **OAuth**: If you are using OAuth 2.0 as your authorisation method, the installation would automatically complete when the OAuth process is finished.
However, if the integration that you have installed requires configurations for activation (configuration set to `True` for activation), you might need to send a request to the [`POST` Activate the installation for a client](/reference/installations/activate-the-installation-for-a-client) endpoint to set the status to `ACTIVATED`. Otherwise, the installation is created with the status `ACTIVATED` by default (configuration is set to `False`).
* **API Key**: You must use the API key that the application has displayed on the configuration page. However, in this method, after successful authorisation, the integration is not automatically installed. Hence, to complete the installation, you must send a request to the [`POST` Create a Client Installation](/reference/installations/create-a-client-installation) endpoint to create the installation in `ACTIVATED` status.
### Installation Status
There are the following possible status of an installation:
| Status | Description |
| :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `AUTHORIZED` | The user has completed the Pleo authorisation process, but the installation is not yet active or ready for use. |
| `ACTIVATED` | The installation is active and ready for use. |
| `INACTIVE` | The installation is inactive and cannot be used. This status occurs after multiple failed attempts to connect to the target application. It also happens if the authorisation method used for the installation is no longer valid, for example using expired refresh tokens for the integration. |
| `PENDING_INTEGRATION` | This status occurs when the integration is synchronising or performing other background tasks. When an installation is in progress, the integration synchronises the user's Pleo account with the target application, so that tax codes, tags, and vendors are all similar in both Pleo and the third-party application. The user must not disconnect the application in this state; else there would be data loss. |
## Installations Workflow
The following section explains a typical application installation workflow:
1. In the Pleo application > **Settings** page, users click the application that they want to integrate with their Pleo account.
2. Based on the authorisation method, the integration is either created in the `ACTIVATED` status or is in the `AUTHORIZED` status:
* If the authorisation method is **OAuth**, the following happens:
* If the configuration is set to `False`: the application is integrated with Pleo and the integration is in the `ACTIVATED` status.
* If the configuration is set to `True`: the integration is in the `AUTHORIZED` status; for activation, the integration sends a request to the `POST Activate the Installation for a Client` endpoint.
* If the authorisation method is **API Keys**, the application is in the `AUTHORIZED` state after successful authorisation. Post authorisation, the integration sends a request to the `POST Create a Client Installation` endpoint. The installation is complete and the application is in the `ACTIVATED` status.
Post activation, when you send a request to the `GET Client Installation` endpoint, the application installation status might change to `PENDING_INTEGRATION`. This happens when the integration is synching tags, tax codes, and vendors between the Pleo account and the client.
# Metadata
Source: https://developers.pleo.io/reference/metadata-1
The `metadata` object enables an integration to send additional information, structured as key value pairs, for either their own use or for reference. For example, the `metadata` parameter in a request to `POST Create a client installation` endpoint includes additional information about an installation.
The information sent in the `Metadata` parameter must conform with the following guidelines:
* You can add a total 50 key value pairs within the following data limits:
* Key: 40 characters limit
* Value: 500 characters limit
* **Must not** include any information that semantically resembles any Pleo feature or function. However, the information in the `metadata` parameter might be displayed in the Pleo product as unstructured data and might be used for full-text search.
## Sample Metadata Use Case
* `GET Get client installation` response: Returns additional information about the specific installation
```json theme={null}
{
"applicationId": "123e4567-e89b-12d3-a456-426614174000",
"createdAt": "2021-01-01T00:00:00Z",
"errorCode": "NOT_ENTITLED",
"id": "123e4567-e89b-12d3-a456-426614174000",
"metadata": {
"key": "value"
},
"resource": "urn:pleo:company:123e4567-e89b-12d3-a456-426614174000",
"status": "ACTIVATED",
"updatedAt": "2021-01-01T00:00:00Z"
}
```
* `GET Retrieves an account in Pleo by its ID`: Returns additional information about the specific account
```json theme={null}
{
"data": {
"archived": false,
"code": "2001",
"companyId": "123e4567-e89b-12d3-a456-426614174006",
"externalId": "External Id",
"id": "123e4567-e89b-12d3-a456-426614174005",
"metadata": {
"key": "value"
},
"name": "Bank Charges",
"taxCodeExternalId": "IVA 20"
}
}
```
# Deprecation and Migration Information
Source: https://developers.pleo.io/reference/notice
# API Deprecation Notice
As part of our ongoing efforts to improve and innovate our product offerings, we have decided to deprecate a number of API endpoints from Pleo's API (now referred to collectively as the Legacy API). The legacy methods will be replaced by new methods over the coming months.
We understand that this change may impact your current workflow, and we want to assure you that we are committed to providing the support you need during this transition. Here are the key details and dates to keep in mind:
* **Deprecation Date:** Effective immediately, the Legacy API is considered deprecated. This means that while the API will remain operational until the end-of-life date, no new features will be added, and only critical security updates and bug fixes will be provided.
* **End-of-Life Date:** After this date, the API endpoints will no longer respond to requests.
* **Migration Support:** Once migration guides are available, they will be communicated alongside the end-of-life date so you can plan accordingly. Pleo Support will be on hand to answer any questions. Further information on the migration path for each use case is provided below.
* **Action Required:** If you are currently using the Legacy API, we recommend beginning the process of migrating your systems as soon as possible. Please review any integrations or applications that depend on the Legacy API and plan accordingly.
## Migration Plans
If your implementation of the Pleo API is for any of the following use cases, find more information about how these functions will be supported going forward.
### Accounting Export
Exporting Pleo expenses into your accounting or ERP system.
| Description | Legacy API | New API | Availability |
| :-------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------- | :----------- |
| Exporting accounting data from Pleo into your accounting or ERP system, including tax codes and receipts. | [Expense API](/reference/pleo-deprecated/expenses/get-an-expense-for-a-company) [Tax Codes API](/reference/pleo-deprecated/tax-codes/get-a-tax-code-for-a-company) [Receipt API](/reference/pleo-deprecated/receipts/get-expense-receipts) | [Export API](/reference/export-api-overview) | Available |
| Syncing cost centers/dimensions between Pleo and the ERP system | [Legacy Tag Groups API](/reference/pleo-deprecated/tag-groups/get-all-tag-groups-belonging-to-the-specified-company) | [Tag Group API](/reference/tags-api/tag-groups/returns-all-tag-groups-for-given-company) | Available |
| Setup of Categories and Accounts | [Legacy Accounts API](/reference/pleo-deprecated/accounts/get-an-account-for-a-company) | [Chart of Accounts API](/reference/accounts/chart-of-accounts-api-overview) | Available |
| Setup of Taxes | [Legacy Tax Codes API](/reference/pleo-deprecated/tax-codes/get-a-tax-code-for-a-company) | [Tax Code API](/reference/tax-codes/create-a-new-tax-code) | Available |
### User Management
Synchonising with your HR system to automate the adding or removal of user from Pleo
| Description | Legacy API | New API | Availability |
| :--------------------------- | :--------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------- |
| Add, update or delete a user | [Employee API](/reference/pleo-deprecated/employees/get-an-employee-for-a-company) | SCIM API | Q3 2026 |
| Teams and review structure | [Teams API](/reference/pleo-deprecated/teams/create-a-team-for-a-company) | [Companies API](/reference/companies/companies-api-overview) and [Employees API](/reference/employees/employees-api-overview) (company and employee info only) | Available |
### Project Management
Ensuring that cost centres within your project management software are represented in Pleo to easily reconcile expenses.
| Description | Legacy API | New API | Availability |
| :--------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------ |
| Synchronise expenses with your project management software | [Expense API](/reference/pleo-deprecated/expenses/get-an-expense-for-a-company) | Accounting Entries API or extended Export API | Available See [migration guides](/deprecation/legacy-expense-api/overview) |
| Synchronise Pleo Tags with cost centers | [Legacy Tag Groups API](/reference/pleo-deprecated/tag-groups/get-all-tag-groups-belonging-to-the-specified-company) | [Tag Groups API](/reference/tags-api/tag-groups/returns-all-tag-groups-for-given-company) | Available |
### Company Balance
| Description | Legacy API | New API | Availability |
| :--------------------------------- | :------------------------------------------------------------------------------------ | :------ | :----------- |
| Get the wallet balance for account | [Company Balance API](/reference/pleo-deprecated/company/get-company-account-balance) | TBD | Q3 2026 |
If you are customer, please direct any questions to your support contact. If you are a partner, you can email [api@pleo.io](mailto:api@pleo.io).
# Postman Collection
Source: https://developers.pleo.io/reference/open-api
This page refers to the Legacy API which is deprecated
## Swagger
Open API 3.0.0 specification for our APIs hosted [here](https://api.pleo.io/rest/v3/swagger.json)
You can use this specification to generate client libraries to interact with our REST API in your language of choice or swagger editor (eg [this](https://editor.swagger.io/)) to test our APIs.
You can also use this definition to create a postman collection for testing purposes as follows:
1. Click [this](https://api.pleo.io/rest/v3/swagger.json) link. This will open the swagger JSON file in the browser.
2. Copy the URL and import it into Postman as follows:
a. Open Postman
b. In your workspace click the Import button:
c. Select the Link tab and paste the swagger json URL into the URL field and click the Continue button:
d. Click the Import button:
e. The API will be imported into your postman collection. Happy requesting!!!
# Pagination
Source: https://developers.pleo.io/reference/pagination
# Overview
Pagination is a technique used to break big datasets into manageable chunks; this helps you to consume the data in a structured manner — if the server returns the entire dataset in a single API response, it could be difficult to read and process the information. In Pleo, we follow offset and cursor based pagination techniques.
## Offset-Based Pagination
In this method, a starting point (`offset`) and a `limit` are mentioned to retrieve data subsets in a paginated format. In **Offset** based pagination, the usual parameters sent in an API **request** are:
* `offset`: Indicates the starting point of the dataset by specifying the number of records to be skipped.
* `limit`: Indicates the maximum number of records to be returned in an API response.
For example, when an integration sends a request to an API endpoint, it must mention the maximum number of records (`limit`) to be returned in the response and must also specify the number of records to skip (`offset`) before starting the count.
`limit` and `offset` are usually used together. `offset` can be zero in the first request because in the subsequent requests, you can mention the number of records to skip, based on the last record sent in the first request.
**Request message example**:
```
https://external.pleo.io/v2/export-jobs?limit=10&offset=10
```
## Cursor-Based Pagination
In this method, a unique identifier or **cursor** indicates the last item returned in a query and this cursor determines the starting point of the next dataset. In **Cursor** based pagination, the usual parameters sent in an API **request** are:
* `limit`: Specifies the maximum number of records to be returned in an API response.
* `before`: Paginates backwards, implying the records before the cursor would be retrieved in an API response.
* `after`: Retrieves records starting after the cursor.
For example, when an integration sends a request to an API endpoint, the server returns the data along with a cursor (`startCursor`/`endCursor`) pointing to either the first or the last item on the page. To continue to the next page, the integration sends the cursor to the server — the cursor acts as the indicator to return data starting after the cursor.
Either `before` or `after` is used in the request.
**Request message example**:
```
https://external.pleo.io/v2/export-jobs?limit=10&after=ThisIsTheEndCursor
```
## API Response | Pagination Attributes
In an API response, the following attributes are included to help you understand what data has been returned from the entire dataset. Based on these details, you can send the subsequent request with appropriate parameters to retrieve the next set of information you require:
* `before`: Indicates the starting point of the current dataset returned in an API response.
* `after`: Indicates the last item of the current dataset returned in an API response.
* `limit`: Indicates the maximum number of records returned in an API response.
* `offset`: Indicates the number of items skipped, implying the starting point of the current dataset.
* `startCursor`: Indicates the starting point of the dataset returned in an API response.
* `endCursor`: Indicates the last item of the dataset returned in an API response.
* `hasNextPage`: If the value is `true`, it implies there is at least one page of data after the dataset returned in the current API response.
* `hasPreviousPage`: If the value is `true`, it implies there is at least one page of data before the dataset returned in the current API response.
* `total`: The total number of records returned in an API response.
In the next request, the server refers to the `endCursor` to retrieve the next batch of items — so in the subsequent request, the dataset starts from where the previous query has finished.
**Response message example**:
```json theme={null}
{
"data": [
{
"address": {
"addressLine1": "string",
"addressLine2": "string",
"country": "string",
"locality": "string",
"postalCode": "string",
"region": "string"
},
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string",
"organizationId": "7bc05553-4b68-44e8-b7bc-37be63c6d9e9"
}
],
"pagination": {
"currentRequestPagination": {
"after": "string",
"before": "string",
"limit": 30,
"offset": 10,
"parameters": {
"property1": ["string"],
"property2": ["string"]
},
"sortingKeys": ["status"],
"sortingOrder": ["ASC"]
},
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "cursor123",
"endCursor": "cursor456",
"total": "120"
}
}
}
```
## Additional Settings | Sorting
Often, sorting is used along with pagination to determine the order in which the items in a dataset are retuned:
* `sortingKeys`: Mentions specific sorting parameters to sort the records returned in an API response.
* `sortingOrder`: Mentions the direction of sorting the records — ascending or descending order.
# Create an account for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/accounts/create-an-account-for-a-company
post /accounts
# Create an account group for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/accounts/create-an-account-group-for-a-company
post /account-groups
# Delete an account for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/accounts/delete-an-account-for-a-company
delete /accounts/{accountId}
# Delete an account group for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/accounts/delete-an-account-group-for-a-company
delete /account-groups/{accountCategoryId}
# Get all company account groups
Source: https://developers.pleo.io/reference/pleo-deprecated/accounts/get-all-company-account-groups
get /account-groups
# Get an account for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/accounts/get-an-account-for-a-company
get /accounts/{accountId}
# Get an account group for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/accounts/get-an-account-group-for-a-company
get /account-groups/{accountCategoryId}
# Get company accounts
Source: https://developers.pleo.io/reference/pleo-deprecated/accounts/get-company-accounts
get /accounts
# Update an account for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/accounts/update-an-account-for-a-company
put /accounts/{accountId}
# Update an account group for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/accounts/update-an-account-group-for-a-company
put /account-groups/{accountCategoryId}
# Get company account balance
Source: https://developers.pleo.io/reference/pleo-deprecated/company/get-company-account-balance
get /companies/balance
# Create an employee for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/employees/create-an-employee-for-a-company
post /employees
# Delete an employee for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/employees/delete-an-employee-for-a-company
delete /employees/{employeeId}
# Get an employee for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/employees/get-an-employee-for-a-company
get /employees/{employeeId}
# Get company employees
Source: https://developers.pleo.io/reference/pleo-deprecated/employees/get-company-employees
get /employees
# Update an employee for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/employees/update-an-employee-for-a-company
put /employees/{employeeId}
# Get an expense for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/expenses/get-an-expense-for-a-company
get /expenses/{expenseId}
# Get expenses for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/expenses/get-expenses-for-a-company
get /expenses
By default does not return expenses with Transaction State AUTHORIZATION or AUTHORIZATION_REVERSAL.
# Update a given list of expenses.
Source: https://developers.pleo.io/reference/pleo-deprecated/expenses/update-a-given-list-of-expenses
put /expenses
At the moment, it is only possible to update expenses' status
# Create an account group for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/experimental/create-an-account-group-for-a-company
post /account-categories
# Delete an account group for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/experimental/delete-an-account-group-for-a-company
delete /account-categories/{accountCategoryId}
# Get all company account groups
Source: https://developers.pleo.io/reference/pleo-deprecated/experimental/get-all-company-account-groups
get /account-categories
# Get an account group for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/experimental/get-an-account-group-for-a-company
get /account-categories/{accountCategoryId}
# Update an account group for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/experimental/update-an-account-group-for-a-company
put /account-categories/{accountCategoryId}
# Returns healtcheck status of OpenAPIs for a given company
Source: https://developers.pleo.io/reference/pleo-deprecated/healthcheck/returns-healtcheck-status-of-openapis-for-a-given-company
get /health
# Get an expense receipt
Source: https://developers.pleo.io/reference/pleo-deprecated/receipts/get-an-expense-receipt
get /expenses/{expenseId}/receipts/{receiptId}
# Get expense receipts
Source: https://developers.pleo.io/reference/pleo-deprecated/receipts/get-expense-receipts
get /expenses/{expenseId}/receipts
# Create a new tag
Source: https://developers.pleo.io/reference/pleo-deprecated/tag-groups/create-a-new-tag
post /tag-groups/{tagGroupId}/tags
This endpoint allows for the creation of a new Tag and its attribute values.
# Create a new tag group.
Source: https://developers.pleo.io/reference/pleo-deprecated/tag-groups/create-a-new-tag-group
post /tag-groups
This endpoint allows for the creation of a new Tag Group with its attribute values
# Delete a tag
Source: https://developers.pleo.io/reference/pleo-deprecated/tag-groups/delete-a-tag
delete /tag-groups/{tagGroupId}/tags/{tagId}
. If this tag is not already in use, the tag will be deleted, otherwise an error will be returned.
# Delete or archive a tag group.
Source: https://developers.pleo.io/reference/pleo-deprecated/tag-groups/delete-or-archive-a-tag-group
delete /tag-groups/{tagGroupId}
This endpoint will delete or archive a Tag Group and all associated attributes and Tags.
# Get a tag from a tag group
Source: https://developers.pleo.io/reference/pleo-deprecated/tag-groups/get-a-tag-from-a-tag-group
get /tag-groups/{tagGroupId}/tags/{tagId}
This endpoint returns a specific tag with it's attribute values for a specified tag group
# Get a tag group
Source: https://developers.pleo.io/reference/pleo-deprecated/tag-groups/get-a-tag-group
get /tag-groups/{tagGroupId}
This endpoint will return a single Tag Group, as well as all of it's attributes
# Get all tag groups belonging to the specified company.
Source: https://developers.pleo.io/reference/pleo-deprecated/tag-groups/get-all-tag-groups-belonging-to-the-specified-company
get /tag-groups
This endpoint returns all tag groups belonging to the company as well as their attributes
# Get all tags for a specified tag group
Source: https://developers.pleo.io/reference/pleo-deprecated/tag-groups/get-all-tags-for-a-specified-tag-group
get /tag-groups/{tagGroupId}/tags
# Update a tag
Source: https://developers.pleo.io/reference/pleo-deprecated/tag-groups/update-a-tag
patch /tag-groups/{tagGroupId}/tags/{tagId}
# Update a tag group
Source: https://developers.pleo.io/reference/pleo-deprecated/tag-groups/update-a-tag-group
patch /tag-groups/{tagGroupId}
# Get a tax code for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/tax-codes/get-a-tax-code-for-a-company
get /tax-codes/{taxCodeId}
# Get tax codes for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/tax-codes/get-tax-codes-for-a-company
get /tax-codes
# Add an employee to a team
Source: https://developers.pleo.io/reference/pleo-deprecated/teams/add-an-employee-to-a-team
put /teams/{teamId}/employees/{employeeId}
# Create a team for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/teams/create-a-team-for-a-company
post /teams
# Delete a team for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/teams/delete-a-team-for-a-company
delete /teams/{teamId}
# Get a team for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/teams/get-a-team-for-a-company
get /teams/{teamId}
# Get all teams for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/teams/get-all-teams-for-a-company
get /teams
# Remove an employee from a team
Source: https://developers.pleo.io/reference/pleo-deprecated/teams/remove-an-employee-from-a-team
delete /teams/{teamId}/employees/{employeeId}
# Update a team for a company
Source: https://developers.pleo.io/reference/pleo-deprecated/teams/update-a-team-for-a-company
put /teams/{teamId}
# Request ID's
Source: https://developers.pleo.io/reference/request-ids
This page refers to the Legacy API which is deprecated
Pleo-Request IDs
Each API request has an associated request identifier. You can find this value in the response headers, under x-pleo-request-Id. If you need to contact us about a specific request, providing the request identifier will ensure the fastest possible resolution.
# Request IDs
Source: https://developers.pleo.io/reference/request-ids-1
Every API request you send to the Pleo platform is assigned a unique identifier. Pleo automatically generates this ID when it receives your request and includes it in the HTTP response headers. This ID acts as a fingerprint for the specific API call within Pleo.
The `request-Id` is returned in each API response, be it a successful or failed attempt; you might inspect the response header to check the details.
## Why Request IDs are Important?
The request ID is the identifier of an API request and thus, helps in troubleshooting:
* **Troubleshooting**: When you contact Pleo support about issues you have encountered with an API request (for example an error, unexpected behaviour, or missing data), our support and engineering team can instantly check the internal logs and identify the API call with the `request-Id`. This significantly reduces the time spent in diagnosing and resolving the problem.
* **Correlation**: Request IDs help you to correlate events; you might store the `request-Id` in your application logs along with the request and the response details. This enables you to trace the request lifecycle, especially if it triggers asynchronous processes or webhooks.
## How to Use Request ID?
* **Logging**: We recommend you to store the `request-Id` in your application logs, along with additional details like timestamp when the request was sent, the request parameters with sensitive and confidential information being masked, and the response status code. This helps you to track the request lifecycle.
* **Contacting Pleo Support**: When you are reporting an issue to the Pleo support team (contact [Pleo support](https://pleohelp.freshdesk.com/en/support/)) about a specific API request, please specify the `request-Id` along with the context — this helps us to instantly identify the problem and serve you better.
# Scopes
Source: https://developers.pleo.io/reference/scopes-2
The following table specifies the different scopes required to access the Tags Group and Tags API endpoints:
| Operation | Scopes |
| :------------------------------------------ | :----------------- |
| List all tag groups for a specific resource | `tag-groups:read` |
| Create new tags groups | `tag-groups:write` |
The Tags API is available for both organisations and companies.
# Scopes
Source: https://developers.pleo.io/reference/scopes-3
The following table specifies the scopes required to access the Tax Code API endpoints:
| Operation | Scopes |
| :-------------------------------------- | :---------------- |
| List all tax codes for a given resource | `tax-codes:read` |
| Create and update tax codes | `tax-codes:write` |
# Create a subscription
Source: https://developers.pleo.io/reference/subscriptions/create-a-subscription
post /v1/subscriptions
Creates a subscription for given event type(s).
# Delete subscription by id
Source: https://developers.pleo.io/reference/subscriptions/delete-subscription-by-id
delete /v1/subscriptions/{id}
Removes the specified subscription record.
# Get subscription activities
Source: https://developers.pleo.io/reference/subscriptions/get-subscription-activities
get /v1/subscriptions/{id}/activities
Fetches subscription activities of a given subscription.
# Get subscription by id
Source: https://developers.pleo.io/reference/subscriptions/get-subscription-by-id
get /v1/subscriptions/{id}
Fetches subscription details by the specific subscription ID.
# Get subscription secret by id
Source: https://developers.pleo.io/reference/subscriptions/get-subscription-secret-by-id
get /v1/subscriptions/{id}/secret
Fetches webhook subscription secret ID by the specific subscription identification code; this is done to verify the webhook event that the customer has subscribed for.
# Get subscriptions
Source: https://developers.pleo.io/reference/subscriptions/get-subscriptions
get /v1/subscriptions
Retrieves a list of subscriptions for the specific event types (registered for the specific company or organisation by the third party application).
# Update a subscription
Source: https://developers.pleo.io/reference/subscriptions/update-a-subscription
put /v1/subscriptions/{id}
Modifies the subscription details.
# Creates a new tag group dimension
Source: https://developers.pleo.io/reference/tags-api/tag-groups/creates-a-new-tag-group-dimension
post /v0/tag-groups/{groupId}/dimensions
# Creates a new tag group resource
Source: https://developers.pleo.io/reference/tags-api/tag-groups/creates-a-new-tag-group-resource
post /v0/tag-groups
# Delete a tag group dimension
Source: https://developers.pleo.io/reference/tags-api/tag-groups/delete-a-tag-group-dimension
delete /v0/tag-groups/{groupId}/dimensions/{dimensionId}
# Deletes the tag group resource by id
Source: https://developers.pleo.io/reference/tags-api/tag-groups/deletes-the-tag-group-resource-by-id
delete /v0/tag-groups/{groupId}
# Get a list of tag group dimensions
Source: https://developers.pleo.io/reference/tags-api/tag-groups/get-a-list-of-tag-group-dimensions
get /v0/tag-groups/{groupId}/dimensions
This endpoint will return a list of tag group dimensions excluding the name and code dimensions which are always present in a tag group. The name and code dimensions are returned only when they do not follow the standard name and code naming convention.
# Get a tag group dimension
Source: https://developers.pleo.io/reference/tags-api/tag-groups/get-a-tag-group-dimension
get /v0/tag-groups/{groupId}/dimensions/{dimensionId}
# Returns all tag groups for given company
Source: https://developers.pleo.io/reference/tags-api/tag-groups/returns-all-tag-groups-for-given-company
get /v0/tag-groups
You need to supply either the company_id or the organization_id query parameter. If you supply both, the company_id will be used. If you supply neither, the request will fail. If you supply the company_id, the response will contain only the tag groups for that company. If you supply the organization_id, the response will contain the tag groups for all companies in the organization. If you supply the company_id and the organization_id, the response will contain only the tag groups for the company.
# Returns all tag groups for given company in an aggregated format
Source: https://developers.pleo.io/reference/tags-api/tag-groups/returns-all-tag-groups-for-given-company-in-an-aggregated-format
get /v0/aggregations/tag-groups
You need to supply either the company_id or the organization_id query parameter. If you supply both, the company_id will be used. If you supply neither, the request will fail. If you supply the company_id, the response will contain only the tag groups for that company. If you supply the organization_id, the response will contain the tag groups for all companies in the organization. If you supply the company_id and the organization_id, the response will contain only the tag groups for the company.
# Returns the tag group resource by id
Source: https://developers.pleo.io/reference/tags-api/tag-groups/returns-the-tag-group-resource-by-id
get /v0/tag-groups/{groupId}
# Update a tag group dimension
Source: https://developers.pleo.io/reference/tags-api/tag-groups/update-a-tag-group-dimension
put /v0/tag-groups/{groupId}/dimensions/{dimensionId}
# Updates the tag group resource by id
Source: https://developers.pleo.io/reference/tags-api/tag-groups/updates-the-tag-group-resource-by-id
put /v0/tag-groups/{groupId}
# Creates a dimension value
Source: https://developers.pleo.io/reference/tags-api/tags/creates-a-dimension-value
post /v0/tags/{tagId}/dimensions/{dimensionId}
# Creates a new tag sub-resource under the given tag group
Source: https://developers.pleo.io/reference/tags-api/tags/creates-a-new-tag-sub-resource-under-the-given-tag-group
post /v0/tag-groups/{groupId}/tags
# Deletes a dimension value
Source: https://developers.pleo.io/reference/tags-api/tags/deletes-a-dimension-value
delete /v0/tags/{tagId}/dimensions/{dimensionId}
# Deletes tag by id
Source: https://developers.pleo.io/reference/tags-api/tags/deletes-tag-by-id
delete /v0/tags/{tagId}
# Fetches dimension values
Source: https://developers.pleo.io/reference/tags-api/tags/fetches-dimension-values
get /v0/tags/{tagId}/dimensions
# Gets a dimension value
Source: https://developers.pleo.io/reference/tags-api/tags/gets-a-dimension-value
get /v0/tags/{tagId}/dimensions/{dimensionId}
# Returns tag by id
Source: https://developers.pleo.io/reference/tags-api/tags/returns-tag-by-id
get /v0/tags/{tagId}
# Returns tags for given tag group
Source: https://developers.pleo.io/reference/tags-api/tags/returns-tags-for-given-tag-group
get /v0/tag-groups/{groupId}/tags
# Search and return aggregated tags
Source: https://developers.pleo.io/reference/tags-api/tags/search-and-return-aggregated-tags
post /v0/aggregations/tags
# Search tags
Source: https://developers.pleo.io/reference/tags-api/tags/search-tags
get /v0/tags
# Updates a dimension value
Source: https://developers.pleo.io/reference/tags-api/tags/updates-a-dimension-value
put /v0/tags/{tagId}/dimensions/{dimensionId}
# Updates tag by id
Source: https://developers.pleo.io/reference/tags-api/tags/updates-tag-by-id
put /v0/tags/{tagId}
# Overview
Source: https://developers.pleo.io/reference/tags-overview
Tags imply cost centres. When you assign tags to an accounting entry, it helps in allocating the correct cost centres to the expense. For example, an expense of the category is recorded in Pleo. This specific accounting entry is assigned the tag. Assignment of tags helps in assessing the amount of cost incurred for specific reasons.
## Important Terminologies
Here are a few terminologies to understand tags, especially in the context of Pleo:
| Tag Group | Tag | Dimension Values |
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tag group is a collection of tags. For example, a tag group is a **Department** and it includes Sales, Marketing, and IT. Each individual value is a specific tag; hence, Sales, Marketing, or IT is a tag indicating a specific department. | Tags are actual cost allocations. For example, if **Projects** is a tag group, **Project A** and **Project B** are individual tags. To explain a tag, we use dimensions; these are like attributes that are crucial in understanding a tag. Attributes of a tag (dimensions) represent column headings in a table, implying they are relevant for all tags of a tag group. And tags are like rows in a table that must contain the dimension values. For example, each project would have a project name and project code. **Project name** and **Project code** are important attributes of each tag. | The value mentioned for each dimension is a **Dimension Value**. For example, project name of Project A is **Open Banking** and project code is **24689**. |
## Characteristics of Tags Managed in Pleo
* Pleo assigns **IDs** to tag groups and tags for correct identification.
* If you are using an ERP/accounting system that supports the tags capability, then for synchronisation of tags between the external ERP/accounting system and Pleo, the integration would send requests to the Tags API endpoints.
* If the external ERP/accounting system does not support inherent tags capability, then you can create custom spreadsheets in CSV format for export of tags from Pleo to the ERP/accounting system.
* You can assign a maximum of **five** tags to an accounting entry. An accounting entry might have tags related to projects, office locations, internal company structure.
* A user can only see non-archived tag groups.
## How are Tags Different from Accounts and Categories?
**Accounts** and **categories** represent **chart of accounts**, where each accounting entry is allocated a specific category; this is helpful in comprehensive financial reporting and analysis. For example, accounting entries could be allocated different categories — card purchase, refund, per diem, and out-of-pocket expenses. For more information on different categories of accounting entries, see [Expense Types](/docs/expense-types).
Whereas, **tags** imply cost centres that are helpful in associating each expense with a cost object, like attributing an expense to a departmental cost or the expense was incurred as part of a budget reserved for specific project initiatives.
## Tag Groups Object
| Object | Type | Description |
| :------------ | :-------- | :--------------------------------------------------------------------------------------------------------------- |
| **archived** | *boolean* | The tag group is no longer used. |
| **code** | *string* | External identifier of the Tag group / Dimension used for mapping to accounting system |
| **companyID** | *string* | Unique identifier of the company the Tag Group belongs to |
| **createdAt** | *string* | Creation date and time. *Example*: 2023-08-23T03:11:48.000Z |
| **id** | *string* | Unique identifier of Tag Group (generated on creation) |
| **metadata** | *string* | Place for API users to store flexible data. Example:`{"externalExtraId":"f302f9ec-6c17-11ee-b962-0242ac120002"}` |
| **name** | *string* | Projects User readable name of Tag Group |
| **updatedAt** | *string* | Date and time of the last update. *Example*: 2023-08-23T03:11:48.000Z |
### Tags Object
| Object | Type | Description |
| :------------ | :-------- | :----------------------------------------------------------------------------------------------------------------- |
| **archived** | *boolean* | This Tag is no longer used |
| **code** | *string* | External identifier of the Tag |
| **createdAt** | *string* | Creation date and time. *Example*: 2023-08-23T03:11:48.000Z |
| **groupId** | *string* | Unique identifier of the Tag Group this Tag belongs to |
| **name** | *string* | User readable name that is used for the possible value within a tag group on an expense *Example*: lunch allowance |
| **updatedAt** | *string* | Date and time of the last update *Example*: 2023-08-23T03:11:48.000Z |
# Overview
Source: https://developers.pleo.io/reference/tax-codes-overview
The Taxes API allow external parties to Create, Read, Update, and Delete a companies tax codes within Pleo.
Tax codes are used to calculate the tax for each expense for proper reporting.
A tax code can be linked to a category (type of spend defined by the user), so all spend classified within this category would have a tax code attached to it. However, a category can have multiple tax codes associated with it, but a default code can be selected for the specific category. This means — when a spender selects a category for the expense, the default code is also selected.
As part of expense review process, before exporting from Pleo, a bookkeeper might change the tax code manually — if the default tax code is incorrect or if the expense needs multiple tax rates according to the line items of the purchase.
### Tax Code Object
| Object | Type | Description |
| :------------------------------ | :-------- | :------------------------------------------------------------------------------------------------------------------------ |
| **accountingIntegrationSystem** | *string* | The accounting system used by the customer |
| **archived** | *boolean* | The flag used to archive or unarchive an account. When set to `true`, account is not visible or usable on the platform. |
| **code** | *string* | The accounting system's internal identifier of the tax code |
| **companyId** | *string* | The Pleo unique identifier of the company the tax code belongs to |
| **createdAt** | *string* | Date and time the tax code was created
*Format*: YYYY-MM-DDTHH:mi:ssZ |
| **Id** | *string* | The unique identifier generated by Pleo for the tax code |
| **ingoingTaxAccount** | *string* | Ingoing tax account is usually used to account for reverse VAT |
| **name** | *string* | Name of the tax code
*Example:* Standard (20%) |
| **outgoingTaxAccount** | *string* | Outgoing tax account usually used to account for reverse VAT |
| **rate** | *number* | Percentage rate applied for this tax code represented in decimals and not the percentage. e.g. 20% tax rate would be 0.20 |
| **type** | *enum* | Classification of this tax code `inclusive` `exclusive` `reverse` |
| **updatedAt** | *string* | Date and time the tax code was last updated
*Format*: YYYY-MM-DDTHH:mi:ssZ |
# Create a new tax code
Source: https://developers.pleo.io/reference/tax-codes/create-a-new-tax-code
post /v0/tax-codes
# Delete an tax code
Source: https://developers.pleo.io/reference/tax-codes/delete-an-tax-code
delete /v0/tax-codes/{taxCodeId}
# Returns a given tax code
Source: https://developers.pleo.io/reference/tax-codes/returns-a-given-tax-code
get /v0/tax-codes/{taxCodeId}
# Returns a list of tax codes
Source: https://developers.pleo.io/reference/tax-codes/returns-a-list-of-tax-codes
post /v0/tax-codes:search
# Update an tax code
Source: https://developers.pleo.io/reference/tax-codes/update-an-tax-code
put /v0/tax-codes/{taxCodeId}
# Vendor API Scopes
Source: https://developers.pleo.io/reference/vendor/vendor-api-scopes
The following table specifies the scopes required to access the Vendors API endpoints:
| Operation | Scopes |
| :---------------------- | :------------ |
| Create a new vendor | vendors:write |
| Fetch a list of vendors | vendors:read |
| Retrieve a vendor | vendors:read |
| Update a vendor | vendors:write |
| Archive a vendor | vendors:write |
| Post a vendor status | vendors:write |
At the moment, the vendor records are stored at the company level.
Hence, if any customer has multiple entities (Organisation level), they would need to use the `company_id` to send request to a specific entity of the organisation.
# Overview
Source: https://developers.pleo.io/reference/vendor/vendors-api-overview
This API is in the early access stage and is available to select pilot partners.
This API enables you to synchronise the list of vendor records, applicable for your business, between the external ERP/accounting system and Pleo. This is crucial as this enables you to tag specific vendors to accounting entries, helping you in assessing the amount of expenses incurred per vendor.
## What is a Vendor?
A vendor is an individual or business entity that sells goods or services to customers.
## Vendor Synchronisation between Pleo and ERP/Accounting System
This is how vendor records are synchronised between Pleo and the ERP/accounting system:
* For importing vendors from the ERP/accounting system to Pleo — the integration sends a request for each individual record to the `POST Create a New Vendor` endpoint.
* Whenever, a new vendor is created in the accounting application, the integration sends a request to the `POST Create a New Vendor` endpoint — this creates the corresponding record in Pleo.
The vendor is created in `Active` state and is identified by its unique code assigned by the ERP/accounting application. There is another scenario, where you can create a vendor in Pleo, without the record existing in the ERP/accounting system. For more information, see [Vendor Creation in Pleo](#vendor-creation-in-pleo).
* If and when any updates are made to a vendor account in the accounting application, the integration sends a request to the `PUT Update a Vendor` endpoint. This ensures that the vendor records in both the accounting system and Pleo are always in sync.
* When any vendor is inactivated or deleted in the ERP/accounting system — the integrations sends a request to `POST Archive a Vendor` endpoint; the status is changed to `archive` state in Pleo.
You cannot remove a vendor in Pleo, unless the record is inactivated or deleted in the ERP/accounting system.
* Similarly, when any vendor is unarchived and set to the `active` status in the ERP/accounting system — a request is sent to the `POST Marks a Vendor as active` endpoint. This activates the vendor record in Pleo.
For optimised results, we recommend the integration to send a request to the `POST Fetches Vendors by Search Criteria` endpoint to compare the records existing between the ERP/accounting system and Pleo; for fetching the search results, the integration must send the specific vendor `codes`. Based on the response received, the integration might create, update, archive, or activate vendors in Pleo.
### Vendor Creation in Pleo
You can also create a new vendor in Pleo without the record existing in the ERP/accounting system — send a manual request to the `POST Create a New Vendor` endpoint. The vendor is created in `Draft` state as the corresponding record is not yet present in the client; also, the `code` identifier is missing in this stage as it is assigned to each record by the ERP/accounting system. A webhook event `v1.vendor.created` is generated and a notification is sent to the integration. Once the record is created in the ERP system, the integration sends a request to the `POST Marks a Vendor as Active` endpoint — this changes the `Draft` state to `Active` and also assigns a unique `code` to the record.
The synchronisation mechanism is run frequently to avoid stale data.
## Vendor API Functions
The Vendor API allows you to perform various operations related to vendors, such as creating, retrieving, updating, archiving, and activating vendors. Each vendor is identified by a unique `code` that is assigned by the external application. Pleo also assigns an `id` to each record for internal identification.
When an accounting entry is linked to a vendor, it helps to bookkeep the accounting entry to the correct vendor in the external accounting system.
## Webhook Notification
When any vendor is created in Pleo (the record is not yet existing in the ERP/accounting system), a webhook event `v1.vendor.created` is triggered. If the integration has subscribed to this event, a notification is sent to the configured endpoint.
For more information on how to configure webhook subscriptions, see [Configuring Webhooks During Integration](/reference/overview-webhooks#configuring-webhooks-during-integration).
### Sample Webhook Notification
```json theme={null}
"Event Headers":
{
"webhook-id":"msg_p5jXN8AQM9LWM0D4loKWxJek",
"webhook-timestamp":1614265330,
"webhook-signature":"v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE="
}
"Event Body":
{
"data": {
"id": "c27ed4d6-3d3d-458e-8f3f-735fdf32a3bc",
"companyId": "00000000-0000-0000-0000-000000000000",
"name": "Pleo Technologies GmbH",
"country": "DE",
"registrationNumber": "",
"taxRegistrationNumber": "DE36538686",
"code": null,
"state": "DRAFT",
"defaultCurrency": "EUR",
"createdBy": "22aff90a-a53d-4f0c-851f-dd4cc85ac947",
"createdAt": "2025-02-21T10:05:43.530450712Z",
"updatedAt": "2025-02-21T10:05:43.530450712Z",
"deletedAt": null,
},
"eventType": "v1.vendor.created",
"eventId": "13d8550d-dd22-46e9-ae18-c171846ba373"
}
```
## Rate Limits
The APIs have a rate limit of 600 requests per minute.
# Versioning
Source: https://developers.pleo.io/reference/versioning
This page refers to the Legacy API which is deprecated
## Backwards Incompatible
Then backwards-incompatible changes are made to the API, a new, dated version is released. Read our API upgrades guide to see our API changelog and to learn more about backwards compatibility.
All requests use your account API settings, unless you override the API version. The changelog lists every available version. Note that by default webhook events are structured according to your account API version, unless you set an API version during endpoint creation.
## Community Libraries
Find some of the many community-supported libraries available for Pleo listed below.
| LANGUAGES / FRAMEWORK | PROJECT | AUTHOR |
| :-------------------- | :------------------------------------------ | :-------------------- |
| Node.JS | Getting Expenses | Victor Reyes Heitmann |
| Java / C# | Getting Expenses in ERP on premise solution | Certified Partner |
# Overview
Source: https://developers.pleo.io/reference/webhooks/overview-webhooks
For Subscriptions API endpoints reference information, see [here](/reference/subscriptions).
When you are integrating with Pleo, you might want to receive real-time notifications for events that occur in Pleo; these webhook notifications can help you organise and manage actions in your applications. The intent is to ensure seamless integration, data synchronisation across multiple applications, and automated workflows, eliminating the need of manual intervention.
To receive webhook notifications, you must configure a receiving endpoint and register a subscription.
To access the Webhook Subscriptions API, you do not require any separate scope. You must use the access token or the API key that was provided during the initial integration set up; just ensure that you have read access to the specific API enabled. For example, if you are creating a subscription for an event relevant with the Export API, your access token or the API key must have read access to the Export API included.
Here is an example for your understanding:
When a user clicks **Export** in the Pleo application, the accounting data of a customer is exported to another application. And, the `v1.export-job.created `event is triggered. If you have subscribed to this event, webhook notifications are automatically sent to the endpoints you have configured. Hence, you do not need to poll `GET https://external.pleo.io/v2/export-jobs`.
In a single subscription request, you can:
* set only one client (third-party application integrated with Pleo) per customer.
* subscribe to multiple events.
## How Webhooks Function?
The following diagram explains the workflow among a customer, third-party integration (third-party application + target endpoint), and Pleo (Pleo APIs + Webhook):
* **Step 1 to step 4 are a one time procedure.**
* **Step 4**: For reference information on **creating subscription for webhook notification**, see [here](/reference/subscriptions/create-a-subscription) .
## Key Features of Pleo Webhooks
* **Event subscription**: If you are a Developer Partner, you can subscribe to specific events in Pleo for which you want to be notified. For example, you can subscribe to the `v1.export.job-created` event, which is triggered when the customer's accounting data is exported to an external application.
* **Real-time notifications**: When an event is triggered, it generates a webhook notification in real-time.
* **Secure communication**: All webhook notifications are sent over HTTPS to ensure secured data transmission.
* **Webhook verification**: The webhook signature and the timestamp mentioned in the request header are checked to verify the authenticity.
## How to Set Up Webhook Notifications?
Perform the following to configure webhook notifications:
1. **Ensure access**: To subscribe to specific webhook events, you need access to the relevant API. In addition, you would also require the corresponding scopes essential to access the API. The scopes are already configured in the JWT token or the API key provided during the initial integration set up. For example, if you want to subscribe to export events, you need the `export-jobs:read` and `export-jobs write` scopes.
You can request for specific scopes during the initial integration set up; hence, you cannot update the scopes when you are creating a webhook subscription.
2. **Configure endpoints**: You must configure the endpoint URL(s) to which you want to send the webhook notifications. For more information, see [Configuring Receiving Endpoints](#configuring-receiving-endpoints).
3. **Subscribe events**: Select the specific events for which you want to send webhook notifications. You can create a new subscription request through the `POST https://external.pleo.io/v1/subscriptions` endpoint. For more information, see [Create a subscription](/reference/subscriptions/create-a-subscription).
4. **Secure endpoints**: We recommend that you set up verification for the incoming Pleo webhook notifications. This ensures protection from potential security threats. For more information, see [Verifying Webhooks](#verifying-webhooks) .
## Configuring Receiving Endpoints
Configure the endpoints to which you want to send webhook notifications. These endpoints must conform with the following:
* Hypertext Transfer Protocol Secure (HTTPS) method
* Available in the public domain
* Accepts POST request with JSON payload
**Sample Endpoint Configuration**
```json theme={null}
{
"status": "ACTIVE",
"eventTypes": [
"v1.export-job.created"
],
"endpointUrl": "",
"customHeaders": {
"Content-Type": "application/json",
"User-Agent": "Chrome/129.0"
}
}
```
**Sample Endpoint Response**:
```json theme={null}
{
"data": {
"id": "2bfde1d4-b3be-4135-960a-914b41ed2dcc",
"status": "ACTIVE",
"eventTypes": [
"v1.export-job.created"
],
"endpointUrl": "https://b7baf754-64e1-4469-ba23-fd7b5988eaf7.mock.pstmn.io/webhook",
"createdAt": "2025-03-24T16:19:23.113886Z",
"updatedAt": "2025-03-24T16:19:23.113886Z"
},
"endpointAuth": {
"type": "NONE"
},
"customHeaders": {
"Content-Type": "application/json",
"User-Agent": "Chrome/129.0"
}
}
```
In this example, the integration has sent a custom header in the API request. Fore more information, see [Configuring Custom Headers in New Subscription Request](/reference/webhooks-authentication-authorisation#configuring-custom-headers-in-new-subscription-request).
## Verifying Webhooks
To ensure the authenticity of the webhook notifications generated from Pleo, we recommend you to verify the webhook signature and timestamp specified in the request header.
### Source IP Addresses
If your receiving endpoint is protected by a firewall, ensure that the IP addresses listed [here](https://docs.svix.com/receiving/source-ips) are whitelisted.
## Configuring Webhooks During Integration
For providing an optimised experience to customers, we recommend that you configure webhook subscriptions during the integration set up. Send a request to the `POST/v1 webhooks` endpoint.
Use the same authentication method (JWT token for OAuth or API key) obtained during the initial integration set up for the customer.
## Webhook Notifications
Once you have created the subscriptions, webhook notifications are automatically sent to the target endpoints. The request message sent to the receiving endpoints contains a HTTPS header and a JSON payload.
### Sample Webhook Notification
```json theme={null}
{
"webhook-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
"webhook-timestamp": 1614265330,
"webhook-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
"payload": {
"data": {
"companyId": "b5fdaec1-1b43-4b62-996a-b0471c3bb336",
"createdAt": "2025-05-23T15:21:32Z",
"createdBy": "e3118b03-691c-4bd4-944e-1a9c1f1031b7",
"expiresIn": 3600,
"id": "b4c4d068-ebbe-45b7-b562-2f16f0deb8b9",
"numberOfItems": 1,
"status": "pending"
},
"eventId": "a257a31e-e8a6-4c36-a98d-451250e5b5c4",
"eventType": "v1.export-job.created"
}
}
```
The `data` object in a webhook notification is an exact representation of the resource (in this example, it is the export job), when the event is generated.
#### **Request Header**
| Key | Section | Type | Description |
| :------------------ | :------ | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `webhook-id` | Header | *string* | A unique message identifier of the webhook message. This identifier is unique across all messages, but will be the same when the same webhook is being resent. |
| `webhook-timestamp` | Header | *string* | The time when the webhook is generated. The time calculation follows [Unix time](https://en.wikipedia.org/wiki/Unix_time%5D). |
| `webhook-signature` | Header | *string* | The Base64 encoded list of signatures (space delimited). |
#### **Request Payload**
The request payload contains the following parameters:
| Parameter | Description |
| :-------------- | :------------------------------------------------------------------------------------------------ |
| `companyId` | Unique identifier of the company for which the webhook event is generated. |
| `createdAt` | Date and time when the event was created. |
| `createdBy` | Unique identifier of the user who created the event. |
| `expiresIn` | The time when the export job expires. Time is calculated in seconds. |
| `id` | Unique identifier of the export job. |
| `numberOfItems` | Number of accounting entries selected for export from Pleo to the external ERP/accounting system. |
| `status` | The current execution status of the export job — `pending`: The export job is has started. |
| `eventId` | Unique identifier of the webhook event. |
| `eventType` | The type of event generated. |
### Successful Webhook Notification Delivery
If the configured endpoint has received the webhook notification, send a `2xx` response indicating successful delivery. Any other response code, including `3xx` imply failures in sending webhook notifications.
### Webhook Notification Delivery Retries
If the first attempt of delivering webhook notification to the configured endpoint fails, succeeding reattempts are made, based on the following schedule, with exponential backoff:
* Immediately
* 5 seconds
* 5 minutes
* 30 minutes
* 2 hours
* 5 hours
* 10 hours
* 10 hours (in addition to the preceding attempt)
Hence, if a webhook notification has been successfully delivered after the third attempt, then it has reached the endpoint after 35 minutes 5 seconds of the first attempt.
# Subscriptions
Source: https://developers.pleo.io/reference/webhooks/subscriptions
The Subscriptions API endpoints enable you to perform `Create`, `Read`, `Update`, and `Delete` (CRUD) subscription operations.
# Webhook Events
Source: https://developers.pleo.io/reference/webhooks/webhook-events
This topic lists the webhook events emitted in the Pleo ecosystem:
* `v1.export.job-created`: When a user clicks **Export** in the Pleo application, an export job is initiated in `pending` status — the `v1.export.job-created` event is generated. If the integration has subscribed to this event, a notification is sent to the configured endpoint.
```json theme={null}
{
"webhook-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
"webhook-timestamp": 1614265330,
"webhook-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
"payload": {
"data": {
"companyId": "b5fdaec1-1b43-4b62-996a-b0471c3bb336",
"createdAt": "2025-05-23T15:21:32Z",
"createdBy": "e3118b03-691c-4bd4-944e-1a9c1f1031b7",
"expiresIn": 3600,
"id": "b4c4d068-ebbe-45b7-b562-2f16f0deb8b9",
"numberOfItems": 1,
"status": "pending"
},
"eventId": "a257a31e-e8a6-4c36-a98d-451250e5b5c4",
"eventType": "v1.export-job.created"
}
}
```
* `v1.vendor.created`: When any vendor is created in Pleo (the record is not yet existing in the ERP/accounting system), a webhook event `v1.vendor.created` is generated. If the integration has subscribed to this event, a notification is sent to the configured endpoint.
```json theme={null}
{
"webhook-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
"webhook-timestamp": 1614265330,
"webhook-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
"payload": {
"data": {
"id": "c27ed4d6-3d3d-458e-8f3f-735fdf32a3bc",
"companyId": "00000000-0000-0000-0000-000000000000",
"name": "Pleo Technologies GmbH",
"country": "DE",
"registrationNumber": "",
"taxRegistrationNumber": "DE36538686",
"code": null,
"state": "DRAFT",
"defaultCurrency": "EUR",
"createdBy": "22aff90a-a53d-4f0c-851f-dd4cc85ac947",
"createdAt": "2025-02-21T10:05:43.530450712Z",
"updatedAt": "2025-02-21T10:05:43.530450712Z",
"deletedAt": null
},
"eventType": "v1.vendor.created",
"eventId": "13d8550d-dd22-46e9-ae18-c171846ba373"
}
}
```
The `data` object in a webhook notification is an exact representation of the resource (in this example, it is the export job), when the event is generated.
# Authentication and Authorisation
Source: https://developers.pleo.io/reference/webhooks/webhooks-authentication-authorisation
To access the Subscriptions API, you can use any of the following (provided during the initial integration set up of the customer):
* JWT token (OAuth)
* API key
- The **JWT token** or the **API key** is generated either for a company or an organisation owning different subsidiaries in a multi-entity set up.
- In your subsequent subscription requests, use the same JWT token (OAuth) or the API key (provided during the initial configuration). This implies that for the subsequent requests, you receive webhook notifications for the same entity that Pleo has authorised. For example, Newday is a credit card organisation. It owns Newday Technology Limited and Newday Cards Ltd. Each JWT token or API key is generated for a specific entity (either Newday Technology Limited or Newday Cards Ltd). To receive webhook notifications for multiple entities, you need to create separate subscriptions using separate JWT tokens or API keys for each entity.
## Authentication Methods for Webhooks Verification
Pleo supports the following:
* **HMAC Signature-Based**: We support and recommend this authentication method. To learn more, click [here](#hmac-signature-based-authentication).
* **Basic authentication**: If the integration does **not** support **HMAC signature-based** authentication, use this method as an alternative option. To learn more, click [here](#basic-authentication).
* **Header-based authentication**: If the integration does **not** support **HMAC signature-based** authentication, you can use header-based authentication as another alternative option. To learn more, click [here](#header-based-authentication).
### HMAC Signature-Based Authentication
In this authentication method, webhook-timsetamp and webhook-signature are sent in the request headers as unique parameters of the webhook notification.
```json theme={null}
{
"webhook-id":"msg_p5jXN8AQM9LWM0D4loKWxJek",
"webhook-timestamp":1614265330,
"webhook-signature":"v1,g0hM9SsE+OTPJTGttmIKtSyZlE3uFJELVlNIOLJ1OE="
}
```
### Basic Authentication
In this method of authentication, the integration can send `username` and `password` in the request header of [Create a Subscription](/reference/subscriptions/create-a-subscription) request. The credentials are also included in the webhook notification sent to the configured endpoint, enabling the consumer system to validate the authenticity of the notification using the **Basic Authentication** mechanisms.
**Endpoint**: POST /v1/subscriptions
**Request message**:
```json theme={null}
{
"status": "ACTIVE",
"eventTypes": ["v1.export-job.created"],
"endpointUrl": "",
"endpointAuth": {
"type": "BASIC",
"credentials": {
"username": "your_username",
"password": "your_password"
}
},
"customHeaders": {
"Content-Type": "application/json"
}
}
```
**Response message**:
```json theme={null}
{
"data": {
"id": "c21a9d0e-f31b-4302-890d-751d400bbe21",
"status": "ACTIVE",
"eventTypes": [
"v1.export-job.created"
],
"endpointUrl": "",
"createdAt": "2025-03-24T16:28:23.090589Z",
"updatedAt": "2025-03-24T16:28:23.090589Z",
"endpointAuth": {
"type": "BASIC",
"credentials": {
"username": "your_username",
"password": "your_password"
}
},
"customHeaders": {
"Content-Type": "application/json"
}
}
}
```
### Header-based Authentication
In this method, the integration can include custom header in the request message to [Create a Subscription](/reference/subscriptions/create-a-subscription) endpoint. The headers can include API keys, bearer tokens, or any other value that the consumer system requires for authenticating the incoming webhook notification.
**Endpoint**: POST /v1/subscriptions:
**Request message**:
```json theme={null}
{
"status":"ACTIVE",
"eventTypes":[
"v1.export-job.created"
],
"endpointUrl":"",
"customHeaders":{
"x-api-key":"xxxxx",
"Authorization":"Bearer your_token",
"Content-Type":"application/json",
"User-Agent":"Chrome/129.0"
}
}
```
**Response message**
```json theme={null}
{
"data": {
"id": "2bfde1d4-b3be-4135-960a-914b41ed2dcc",
"status": "ACTIVE",
"eventTypes": [
"v1.export-job.created"
],
"endpointUrl": "",
"createdAt": "2025-03-24T16:19:23.113886Z",
"updatedAt": "2025-03-24T16:19:23.113886Z"
},
"endpointAuth": {
"type": "NONE"
},
"customHeaders": {
"x-api-key": "xxxxx",
"Authorization": "Bearer your_token",
"Content-Type": "application/json",
"User-Agent": "Chrome/129.0"
}
}
```
#### **Configuring Custom Headers in New Subscription Request**
While creating a new subscription request, the integration can send a custom header in the API request. For example, the integration can specify the content-type or the user-agent it wants to receive in the API response.
**Endpoint**: [Create a Subscription](/reference/subscriptions/create-a-subscription)
**Request message**:
```json theme={null}
{
"status": "ACTIVE",
"eventTypes": ["v1.export-job.created"],
"endpointUrl": "",
"customHeaders": {
"Content-Type": "application/json",
"User-Agent": "Chrome/129.0"
}
}
```
**Response message**:
```json theme={null}
{
"data": {
"id": "2bfde1d4-b3be-4135-960a-914b41ed2dcc",
"status": "ACTIVE",
"eventTypes": [
"v1.export-job.created"
],
"endpointUrl": "",
"createdAt": "2025-03-24T16:19:23.113886Z",
"updatedAt": "2025-03-24T16:19:23.113886Z"
},
"endpointAuth": {
"type": "NONE"
},
"customHeaders": {
"Content-Type": "application/json",
"User-Agent": "Chrome/129.0"
}
}
```