This playbook covers the end-to-end integration of five major T&E platforms (SAP Concur, Expensify, Navan, Brex, and Ramp) with enterprise ERP systems. It focuses on the data flow from expense creation through GL journal posting and reimbursement. This card does NOT cover AP invoice processing, procurement-to-pay workflows, or payroll integration for reimbursement via paycheck.
| System | Role | API Surface | Direction |
|---|---|---|---|
| SAP Concur | T&E platform -- expense reports, travel booking, receipt capture | Financial Integration API v4 (REST) | Outbound to ERP |
| Expensify | T&E platform -- expense reports, corporate card, receipt scanning | Integration Server API (REST) | Outbound to ERP |
| Navan | T&E + travel platform -- expense, travel booking, corporate card | Expense API v1, SFTP | Outbound to ERP |
| Brex | Corporate card + expense -- card transactions, receipt matching | Accounting API v1 (REST) | Bidirectional with ERP |
| Ramp | Corporate card + expense -- card transactions, AP automation | Developer API v1 (REST) | Bidirectional with ERP |
| ERP (Target) | Financial system of record -- GL, AP, AR | Varies (REST, OData, SOAP, File Import) | Inbound from T&E |
| iPaaS (Optional) | Integration orchestrator -- Workato, Boomi, MuleSoft, Celigo | Varies | Orchestrator |
| T&E Platform | Export Method | Format | Real-time? | Pre-built ERP Connectors | Custom API? |
|---|---|---|---|---|---|
| SAP Concur | Financial Integration API v4 | JSON | Near-real-time (poll) | SAP S/4HANA, Oracle, NetSuite, Dynamics 365, Sage Intacct | Yes |
| SAP Concur | Standard Accounting Extract (SAE) | Flat file | Batch (scheduled) | Any ERP with file import | N/A |
| Expensify | Integration Server API | JSON | On-demand | NetSuite, QBO, Xero, Sage Intacct, Oracle | Yes |
| Navan | Direct Integration | JSON | Near-real-time | NetSuite, Sage Intacct, QBO | Limited |
| Navan | SFTP Export | CSV/JSON | Batch (scheduled) | Any ERP with file import | N/A |
| Brex | Accounting API v1 | JSON | Real-time (two-way) | NetSuite, QBO, Xero, Sage Intacct | Yes |
| Ramp | Developer API v1 | JSON | Real-time (sync) | NetSuite, Oracle Fusion, Sage Intacct, QBO, Xero | Yes |
| T&E Platform | Limit Type | Value | Notes |
|---|---|---|---|
| SAP Concur | Max financial documents per fetch | 100 per page | Paginate with nextPage link |
| SAP Concur | Expense Report API v4 query | 100 reports per page | Use start parameter for pagination |
| Expensify | Max report export batch | 1 concurrent request per credential pair | Queue exports sequentially |
| Brex | Max transactions per page | 1,000 | Cursor-based pagination |
| Ramp | Max records per page | 100 | Cursor-based pagination |
| Ramp | Accounting sync batch | 500 GL codes per sync | Split larger code sets |
| T&E Platform | Limit Type | Value | Window | Notes |
|---|---|---|---|---|
| SAP Concur | API calls (OAuth app) | 24,000 requests/hr | Rolling hourly | Shared across all API surfaces |
| Expensify | API requests | No published hard limit | N/A | Subject to fair-use throttling |
| Brex | API calls | Rate-limited per endpoint | Per minute | 429 response with retry-after header |
| Ramp | API calls | Rate-limited per endpoint | Per minute | 429 response with retry-after header |
| Navan | SFTP export frequency | Configurable (min 1hr) | Scheduled | Direct API limits not publicly documented |
| T&E Platform | Auth Method | Mechanism | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|---|
| SAP Concur | OAuth 2.0 | JWT bearer (company-level) or Auth Code (user-level) | Access: 1h, Refresh: 6 months | Yes | Company JWT for Financial Integration API |
| Expensify | Static credentials | partnerUserID + partnerUserSecret | Unlimited | No | One pair per integration |
| Navan | API Key | Bearer token in header | Long-lived | No | Issued by Navan support |
| Brex | OAuth 2.0 | Authorization Code flow | Access: 1h | Yes | Scopes: accounting.read, accounting.write |
| Ramp | OAuth 2.0 | Authorization Code flow | Access: 1h | Yes | Scopes: accounting:read, accounting:write |
approved status. You cannot pull pending, submitted, or recalled reports.START -- Integrate T&E platform with ERP
|
+-- Which T&E platform?
| +-- SAP Concur
| | +-- SAP S/4HANA or ECC as ERP?
| | | +-- YES --> Use Concur-SAP standard connector (pre-built, certified)
| | | +-- NO --> Use Financial Integration API v4
| | +-- Need pre-approval data?
| | +-- YES --> Reports API v4 (separate from Financial Integration)
| | +-- NO --> Financial Integration API v4 only
| |
| +-- Expensify
| | +-- NetSuite, QBO, Xero, or Sage Intacct?
| | | +-- YES --> Use Expensify native integration
| | | +-- NO --> Use Integration Server API
| | +-- Export format: Journal entries, Vendor bills, or Credit card charges
| |
| +-- Navan
| | +-- NetSuite, Sage Intacct, or QBO?
| | | +-- YES --> Use Navan direct integration
| | | +-- NO --> SFTP export + custom file import to ERP
| |
| +-- Brex
| | +-- Need real-time sync?
| | | +-- YES --> Accounting API (bidirectional)
| | | +-- NO --> CSV batch export
| |
| +-- Ramp
| +-- Oracle Fusion, NetSuite, Sage Intacct, or QBO?
| | +-- YES --> Use Ramp native accounting sync
| | +-- NO --> Developer API v1 + custom mapping
|
+-- Corporate card reconciliation needed?
| +-- YES --> Clearing account pattern (see Quick Reference)
| +-- NO --> Expense report journal posting only
|
+-- Multi-entity / intercompany?
+-- YES --> Configure entity mapping; route journals per entity
+-- NO --> Single-entity standard flow
| Step | Source | Action | Target | Data Objects | Failure Handling |
|---|---|---|---|---|---|
| 1 | Employee | Capture receipt, create expense line | T&E Platform | Expense entry, receipt image | Auto-retry OCR; manual entry fallback |
| 2 | T&E Platform | Apply policy rules, auto-code GL account | T&E Platform | Expense type → GL mapping | Flag out-of-policy items for review |
| 3 | T&E Platform | Route for approval | T&E Platform | Approval workflow, delegation | Escalation after SLA breach |
| 4 | T&E Platform | Mark report approved, lock for export | T&E Platform | Approved expense report | Reopen if rejected |
| 5 | T&E API | Export approved report as financial document | iPaaS / Custom | Journal lines (debit/credit) | Retry 3x, then DLQ |
| 6 | iPaaS / Custom | Transform T&E data to ERP journal format | ERP | GL journal entry | Validate GL codes before posting |
| 7 | ERP | Post journal entry, create payable | ERP | GL journal, AP voucher | Duplicate check on external ref ID |
| 8 | ERP | Process reimbursement | Bank / Payroll | Payment instruction | Reconcile against posted journal |
| 9 | iPaaS / Custom | Post confirmation back to T&E platform | T&E Platform | Posting status, ERP doc ID | Update status to posted/failed |
| 10 | Card Network | Feed corporate card transactions | T&E Platform | Card transaction data | Match to expenses; flag orphaned |
Map your ERP chart of accounts into the T&E platform. Every T&E system maintains its own expense type list that must map 1:1 to GL natural accounts. [src6, src7]
Verify: Export a test expense report and confirm every line maps to a valid GL code. Zero unmapped lines = ready for production.
Each platform uses a different auth mechanism. SAP Concur uses OAuth 2.0 JWT bearer; Expensify uses static credentials; Brex and Ramp use OAuth 2.0 Authorization Code flow. [src1, src3]
# SAP Concur: Exchange refresh token for access token
curl -X POST "https://us2.api.concursolutions.com/oauth2/v0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN"
Verify: Response includes access_token and token_type: "Bearer".
The Financial Integration API v4 returns approved documents ready for ERP posting. Poll every 15-60 minutes. [src1, src2]
curl -X GET "https://us2.api.concursolutions.com/financialintegration/fi/v4/companies/transactiontypes/expense/transactions?limit=100" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Verify: Each document has systemId for idempotent posting.
Map T&E financial document to ERP journal schema. Core pattern: DEBIT expense accounts, CREDIT AP payable (reimbursable) or clearing account (corporate card). [src2, src6]
Verify: Sum of debits == sum of credits for every journal entry.
Post transformed journal using the T&E document ID as external reference for idempotent duplicate prevention. [src7]
Verify: ERP returns 204 (created) or 409 (duplicate).
Confirm back so the document is locked and marked as posted. [src1]
Verify: Document no longer appears in future Financial Integration API polls.
Card reconciliation uses a clearing account pattern: card transactions flow through T&E platform, export as DEBIT expense / CREDIT clearing; monthly settlement clears the clearing account against the bank. [src6]
Verify: At month-end, clearing account balance = sum of approved-but-not-settled card transactions.
# Input: Expensify API credentials, date range
# Output: List of journal entries ready for ERP posting
import requests, json
def export_expensify_reports(partner_id, partner_secret, start_date, end_date):
payload = {
"type": "get",
"credentials": {"partnerUserID": partner_id, "partnerUserSecret": partner_secret},
"inputSettings": {
"type": "combinedReportData",
"filters": {"status": "APPROVED", "startDate": start_date, "endDate": end_date}
}
}
resp = requests.post(
"https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations",
data={"requestJobDescription": json.dumps(payload)}, timeout=60
)
reports = resp.json()
journals = []
for report in reports:
lines = []
for expense in report["expenses"]:
lines.append({
"account": expense["category"],
"debit": float(expense["amount"]) / 100, # Cents to dollars
"description": expense["merchant"],
})
total = sum(l["debit"] for l in lines)
credit_acct = "2100-200" if report.get("hasCompanyCard") else "2100-100"
lines.append({"account": credit_acct, "credit": total})
journals.append({"reference": report["reportID"], "lines": lines})
return journals
// Input: Ramp OAuth token, last sync timestamp
// Output: New card transactions with GL coding for ERP posting
const axios = require('axios'); // ^1.7.0
async function syncRampTransactions(accessToken, lastSyncTime) {
const transactions = [];
let cursor = null;
do {
const params = new URLSearchParams({ from_date: lastSyncTime, page_size: '100' });
if (cursor) params.set('start', cursor);
const resp = await axios.get(
`https://demo-api.ramp.com/developer/v1/transactions?${params}`,
{ headers: { Authorization: `Bearer ${accessToken}` }, timeout: 30000 }
);
for (const txn of resp.data.data) {
transactions.push({
reference: txn.id, date: txn.user_transaction_time,
merchant: txn.merchant_name, amount: txn.amount,
gl_account: txn.accounting_field_selections?.find(f => f.type === 'GL_ACCOUNT')?.external_id,
cost_center: txn.accounting_field_selections?.find(f => f.type === 'COST_CENTER')?.external_id,
});
}
cursor = resp.data.page?.next;
} while (cursor);
return transactions;
}
# Push GL account list from ERP to Brex (required before sync)
curl -X POST "https://platform.brexapis.com/v2/accounting/accounts" \
-H "Authorization: Bearer YOUR_BREX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"accounts": [
{"id": "6200-100", "name": "Travel - Air", "type": "EXPENSE"},
{"id": "6200-200", "name": "Travel - Lodging", "type": "EXPENSE"},
{"id": "2100-200", "name": "Corp Card Clearing", "type": "LIABILITY"}
]}'
# Fetch coded transactions ready for ERP posting
curl -X GET "https://platform.brexapis.com/v2/transactions/card?posted_at_start=2026-03-01T00:00:00Z" \
-H "Authorization: Bearer YOUR_BREX_TOKEN"
| T&E Field (Generic) | SAP Concur | Expensify | Ramp/Brex | ERP Target | Gotcha |
|---|---|---|---|---|---|
| Expense amount | journalAmount | amount (cents) | amount (cents) | Debit amount | Expensify/Ramp/Brex use cents; divide by 100 |
| GL account | accountCode | category | gl_account (external_id) | Natural account code | Must match ERP chart exactly |
| Cost center | orgUnit1 | tag | cost_center (external_id) | Cost center dimension | Tag mapping varies by Expensify policy |
| Employee name | employeeName | submitterEmail | card_holder.full_name | Vendor/payee name | Some ERPs require employee as vendor record |
| Transaction date | transactionDate | created | user_transaction_time | Journal date | T&E captures swipe date; ERP may want posting date |
| Currency code | transactionCurrencyCode | currency | currency_code | Transaction currency | Multi-currency requires rate agreement |
| Payment type | paymentType | reimbursable flag | Card-only (always company-paid) | Credit account selection | Determines clearing account vs AP payable |
| Report ID | systemId | reportID | id | External reference | Critical for idempotent duplicate prevention |
journalAmount sign indicates debit (+) or credit (-). Expensify and Ramp always report positive amounts. [src1, src3]amount: 4250. Concur uses decimal. Always check the API docs for amount format. [src3, src4]DeptA:ProjectX:TaskY. [src3]| Source | Code | Meaning | Cause | Resolution |
|---|---|---|---|---|
| SAP Concur | 401 | Unauthorized | Expired token or revoked refresh token | Re-authenticate; check refresh token (6 months non-use) |
| SAP Concur | 404 | Document not found | Report recalled or deleted after approval | Skip document; log for audit |
| Expensify | 500 | Server error | Concurrent export on same credentials | Wait 60s and retry; serialize exports |
| Brex/Ramp | 429 | Rate limit exceeded | Too many API calls per minute | Read Retry-After header; exponential backoff |
| ERP (NetSuite) | 409 | Duplicate record | Journal with same externalId already posted | Log as success (idempotent); do not retry |
| ERP (SAP) | BAPI error | GL account invalid | Mapped to inactive or blocked GL code | Update mapping; validate GL codes monthly |
| Any | Timeout | Request timed out | Large batch or ERP under load | Reduce batch size; circuit breaker |
Validate all GL codes against ERP chart of accounts weekly via automated sync. [src6]Set policy requiring card transactions reported within 60 days. Auto-create entries for transactions >90 days old. [src6, src7]Configure both systems to use same rate source and timing. Add FX gain/loss line. [src7]Auto-delegate after 3 business days; auto-approve under $500 after 7 days with audit flag. [src7]Always use T&E document ID as externalId. Implement upsert-or-skip logic. [src1]// BAD -- posts raw card feed to GL, skipping approval, GL coding, and receipt matching
cardFeed.forEach(txn => {
postJournal({ debit: { account: "6200-000", amount: txn.amount },
credit: { account: "2100-200", amount: txn.amount } });
});
// GOOD -- only approved, coded, policy-compliant reports export to ERP
const approved = await pollApprovedReports();
for (const report of approved) {
const journal = transformToJournal(report); // Proper GL codes per entry
validateGLCodes(journal);
await postToERP(journal); // externalId for idempotency
await confirmPosting(report.id); // Lock in T&E platform
}
// BAD -- posts a journal for every expense line; thousands of tiny journals
onExpenseCreated(expense => { postJournal(transformSingleExpense(expense)); });
// GOOD -- polls for approved reports every 30 min; batches for manageable volume
schedule("*/30 * * * *", async () => {
const approved = await pollApprovedReports();
const journals = approved.map(transformToJournal);
await batchPostToERP(journals.filter(validateGLCodes));
await confirmPostings(journals.map(j => j.reference));
});
// BAD -- requires code deployment to change GL mappings
const MAP = { "Airfare": "6200-100", "Hotel": "6200-200" };
function getGL(type) { return MAP[type] || "6999-999"; }
// GOOD -- finance team can update without code deployment
async function getGL(type) {
const row = await db.query("SELECT gl_code FROM expense_gl_mapping WHERE expense_type = $1 AND active = true", [type]);
if (!row) throw new Error(`No active GL mapping for: ${type}`);
return row.gl_code;
}
Monitor weekly. Aging > 30 days = investigate. > 90 days = write policy for auto-reporting. [src6]Sync GL code list from ERP to T&E platform monthly. [src7]Write a normalization layer per platform with unit tests. [src1, src3]Check both report-level AND line-level approval status before export. [src1]Always set externalId using T&E document/report ID. Implement upsert-or-skip. [src7]Always close the loop with POST confirmation back to T&E after ERP posting. [src1]# SAP Concur: Check for pending financial documents
curl -X GET "https://us2.api.concursolutions.com/financialintegration/fi/v4/companies/transactiontypes/expense/transactions?limit=10" \
-H "Authorization: Bearer YOUR_TOKEN"
# Expected: JSON array of pending docs; empty = all posted
# SAP Concur: Verify OAuth token validity
curl -X GET "https://us2.api.concursolutions.com/profile/v1/me" \
-H "Authorization: Bearer YOUR_TOKEN"
# Expected: 200 with user profile; 401 = token expired
# Expensify: Test credentials and list available reports
curl -X POST "https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations" \
-d 'requestJobDescription={"type":"get","credentials":{"partnerUserID":"YOUR_ID","partnerUserSecret":"YOUR_SECRET"},"inputSettings":{"type":"reportStatus"}}'
# Ramp: Check API and list recent transactions
curl -X GET "https://demo-api.ramp.com/developer/v1/transactions?page_size=5" \
-H "Authorization: Bearer YOUR_TOKEN"
# ERP (NetSuite): Verify GL account exists
curl -X GET "https://YOUR_ACCOUNT.suitetalk.api.netsuite.com/services/rest/record/v1/account?q=acctNumber IS '6200-100'" \
-H "Authorization: Bearer YOUR_TOKEN"
| Platform | API Version | Release Date | Status | Breaking Changes | Notes |
|---|---|---|---|---|---|
| SAP Concur | Financial Integration API v4 | 2023-06 | Current (GA) | Replaced v3 Extract API | v3 Extract still operational |
| SAP Concur | Expense Report API v4 | 2022-01 | Current (GA) | User v1 Form Fields sunset June 2026 | Migrate before deadline |
| Expensify | Integration Server v1 | 2015 | Current (GA) | None recent | Stable but limited additions |
| Navan | Expense API v1 | 2024 | Current (GA) | N/A | SFTP primary method |
| Brex | Accounting API v1 | 2025-01 | Current (GA) | Legacy API keys incompatible | Re-authenticate with OAuth 2.0 |
| Ramp | Developer API v1 | 2024-06 | Current (GA) | None | Accounting Agent added 2025 |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Employee expense reports need GL journal posting | Vendor invoices / AP processing | AP Automation Integration playbook |
| Corporate card reconciliation against expense reports | Simple card-to-bank reconciliation without GL coding | Bank reconciliation in your ERP |
| Multi-level approval workflows gate GL posting | Single-person company, no approval needed | Direct card feed import in QBO/Xero |
| >50 employees submitting expenses monthly | <10 employees with occasional expenses | Manual entry (automation ROI is negative) |
| Multi-entity or multi-currency processing | Single entity, single currency, domestic only | Native T&E platform export (no custom integration) |
| Per diem, mileage, or GST/VAT reclaim needed | Standard reimbursement without tax complexity | Simpler T&E native export |
| Capability | SAP Concur | Expensify | Navan | Brex | Ramp |
|---|---|---|---|---|---|
| ERP Integration API | Financial Integration API v4 | Integration Server API | SFTP + limited API | Accounting API v1 (bidirectional) | Developer API v1 + Accounting Agent |
| Pre-built connectors | SAP, Oracle, NetSuite, D365, Sage | NetSuite, QBO, Xero, Sage | NetSuite, Sage Intacct, QBO | NetSuite, QBO, Xero, Sage | NetSuite, Oracle Fusion, Sage, QBO, Xero |
| Real-time sync | Near-real-time (poll) | On-demand (API call) | Batch (SFTP schedule) | Real-time (two-way) | Real-time (auto-sync) |
| AI auto-coding | SmartExpense (receipt OCR) | SmartScan (receipt OCR) | Limited | AI accounting (Jan 2026) | Accounting Agent (AI) |
| Corporate card | Card feed integration | Expensify Card | Navan Card | Brex Card (native) | Ramp Card (native) |
| Travel booking | Concur Travel (integrated) | No | Navan Travel (core) | No | No |
| Multi-entity | Yes (advanced config) | Yes (policy-level) | Yes | Yes | Yes |
| Multi-currency | Yes | Yes | Yes | Yes | Yes |
| Posting confirmation | Yes (API callback) | No (one-way export) | No (SFTP-based) | Yes (two-way sync) | Yes (sync status) |
| Best for | Large enterprise, SAP shops | SMB, simple workflows | Travel-heavy companies | Card-first, real-time finance | Card-first, AI-driven automation |