ERP charge sync LIVE

Pull consolidated charges out of OpenIRIS and post them into SAP, Oracle Fusion, NetSuite or Workday as journal entries — with a dedup fence, because your ERP probably doesn't have one.

Scope required: billing:read (covers reading charges and recording exports). Add billing:write only if your integration also sets the sign-off flag — usually a person does that in OpenIRIS, not your job.

The loop

  1. Pull new/changed charges since your last run.
  2. Transform each charge into your ERP's journal-entry shape (mapping below).
  3. Post it to the ERP.
  4. Acknowledge the export back to OpenIRIS with the ERP document number — this is your dedup fence and flips the charge's status to exported.

1. Pull — delta sync

GET /v1/charges?filter[created_at][gte]=2026-08-01T00:00:00Z&sort=created_at&limit=200

Page with links.next; persist the newest created_at you've seen as the next run's watermark. To pull only what's not yet posted, add filter[status]=invoiced (or unbilled, depending on where your institution draws the "ready" line).

If your facility signs charges off before billing, ask for the confirmed ones instead and let the billing admin decide when a charge is ready:

GET /v1/charges?filter[status]=unbilled&filter[confirmed]=true&sort=id&limit=200

Or watermark on the sign-off itself with filter[confirmed_at][gte]=…. confirmed_at is set by whoever checks the charges — see the changelog for the write endpoints — and is null until they do.

Watermark on created_at for now. filter[updated_at][gte] exists, but historical rows are still being backfilled — until the changelog announces completion, updated_at-based delta sync under-reports. created_at is reliably populated on every row.

A charge

{
  "id": 98888864,
  "source": "booking",
  "source_id": 24722216,
  "booking_id": 3416659,
  "provider_id": 1300,
  "user_id": 20117,
  "group_id": 512,
  "currency": "EUR",
  "total": 215.0511,
  "is_waived": false,
  "invoice_id": 33,
  "status": "invoiced",
  "confirmed_at": "2026-07-02T10:15:00Z",
  "period_start": "2026-06-30T07:00:00Z",
  "period_end": "2026-06-30T09:00:00Z",
  "created_at": "2026-06-30T09:12:44.153Z",
  "links": { "self": "…/v1/charges/98888864" }
}

2. Transform — SAP Journal Entry mapping

Target Journal Entry Post (API_JOURNALENTRYITEMBASIC) — OpenIRIS charges are internal cost recovery, not supplier invoices. Oracle GL Journal Batch / NetSuite Journal Entry / Workday Manual Journal follow the same shape.

SAP fieldSourceNotes
CompanyCodeyour configMap provider → company code on your side
PostingDate / DocumentDateperiod_end or invoice_dateWatch fiscal-period cutoffs (below)
DocumentReferenceIDcharge id≤35 chars; your dedup key inside SAP
GLAccountyour mapping tableTypically resource/provider → GL on your side; GET /v1/price-types/{id}/items lists the price item codes a provider bills with if you map by price item
CostCentercost_center.provider_code*≤10 alphanumeric — matches SAP's limit
Amount + currencytotal + currencyDebit user cost center, credit facility revenue GL

* cost_center is live: provider_code is the provider cost center's code, resolved from the booking or request the charge came from, and is null when nothing is assigned. Filter on it with filter[cost_center.provider_code]=. Institutions that invoice one document per cost center group the pulled charges by this field; the quarterly invoice created in OpenIRIS stays the freeze step, and filter[invoice_id]= pulls exactly the charges on it.

SAP has no native duplicate check on Journal Entry Post (SAP KBA 2817042). A retried job posts the same charge twice unless you fence it — that is exactly what step 4 is for.

Fiscal periods: SAP posting periods close monthly. A delayed export can land in a closed period — decide whether late charges post to the original period date or the run date, and be consistent.

3–4. Post, then acknowledge

After the ERP accepts the entry, record the acknowledgement (note the Idempotency-Key — required on all writes):

POST /v1/charges/98888864/exports
Idempotency-Key: 7f3a1c…
Content-Type: application/json

{ "external_system": "SAP", "external_id": "4900001234", "notes": "FY26 P06" }

GET /v1/charges/{id}/exports returns the full history — the audit answer to "where did this charge land, and when?". On your next pull, filter[status]=invoiced naturally excludes everything already exported.

Crash-safe ordering

Post to the ERP before acknowledging. If your job dies between the two, the next run re-posts the charge — and SAP-side dedup on DocumentReferenceID (the charge id you set in step 2) catches it. If you acknowledge first and die, the charge silently never reaches the ERP. Post → acknowledge, always in that order.