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
- Pull new/changed charges since your last run.
- Transform each charge into your ERP's journal-entry shape (mapping below).
- Post it to the ERP.
- 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.
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" }
}
idis a synthetic integer, stable and unique across charge sources — treat it as opaque.source+source_idcarry the domain identity (bookingorrequest).statuslifecycle:unbilled→invoiced→exported;waivedcharges are zeroed by the facility and should not be posted.confirmed_atis a separate axis fromstatus, not a value of it: it is when a billing admin signed the charge off, and it stays set through invoicing and export.nullmeans nobody has signed it off — which, at a facility that uses the flag, means don't post it yet.currencyis ISO 4217. Amounts keep source precision — do not re-round before your ERP requires it.GET /v1/charges/{id}/line-itemsgives the per-usage breakdown when your posting is line-level.
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 field | Source | Notes |
|---|---|---|
CompanyCode | your config | Map provider → company code on your side |
PostingDate / DocumentDate | period_end or invoice_date | Watch fiscal-period cutoffs (below) |
DocumentReferenceID | charge id | ≤35 chars; your dedup key inside SAP |
GLAccount | your mapping table | Typically 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 |
CostCenter | cost_center.provider_code* | ≤10 alphanumeric — matches SAP's limit |
Amount + currency | total + currency | Debit 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.
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" }
201— recorded; the charge'sstatusbecomesexported.200— this (system, external id) pair was already recorded: your retry hit the fence, nothing double-posted. Carry on.409— the charge is waived; it should not have been posted.
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.