1. Overview
- What: one-way sync, Hyperline (billing) → Upflow (A/R). Dedicated Node.js service, built multi-merchant from day one.
- Hosting & operations: Reviaflow infrastructure (EU), per the agreement of 15 Jul 2026. Handover-ready codebase for a possible future re-hosting on Upflow infrastructure.
- Creation ownership (settled, 17 Jul): the connector creates customers and contacts. Upflow's Salesforce connection is optional, secondary enrichment, out of connector scope (see § 13).
- Org topology (settled, 17 Jul): three separate Upflow organizations, one per Hyperline invoicing entity (Kolsquare · Kolsquare Italy · Storyclash GmbH). One API key pair per org; custom-field definitions provisioned in each org.
- Amounts: minor units (cents) on both sides. No conversion.
2. Architecture & sync model
| Component | Behaviour | Layer |
|---|---|---|
| Runtime | Always-on Node.js service, isolated environment on EU infrastructure, secrets in encrypted store, deploy pipeline, monitoring. | PLATFORM |
| Incremental sync | Hourly poll on Hyperline updated_at cursor (payment recording bumps it — verified on 2,389 paid invoices, zero counter-example). | PLATFORM |
| Pre-dunning run | Dedicated run every morning before Upflow's dunning scheduler, so an invoice recorded as paid in Hyperline is never dunned. | PLATFORM |
| Nightly sweep | Consistency pass: open invoices cross-checked on both sides; detects records deleted in Hyperline (invisible to incremental polling) and removes them from Upflow. | PLATFORM |
| Idempotence | Every write is an upsert on externalId. Re-running a sync never duplicates. Safe recovery = re-run. | PLATFORM |
| Dry-run mode | Full run with zero writes, complete diff output. Reviewed together before any production write. | PLATFORM |
| Org routing | Every customer and its attached objects routed by invoicing_entity_id to the matching Upflow org (×3). | KOLSQUARE config on a PLATFORM mechanism |
| Initial import | All open invoices regardless of age + ~1 year of settled history. Any open invoice carries its complete payment & credit chain even beyond 1 year (outstanding amounts must match Hyperline exactly). Runs in Test Mode. | KOLSQUARE |
| Webhooks | Not in v1 (see § 11). Polling is the backbone: Hyperline webhooks carry no partial payments and no delivery/order guarantee. HTTPS endpoint reserved at infra level for a later real-time layer. | PLATFORM (v2 option) |
3. Object-level map
| # | Hyperline object | → Upflow object | Write path | Idempotence key | Kolsquare volume |
|---|---|---|---|---|---|
| 1 | Customer (native) | Customer (native) | POST /customers (upsert) | externalId = Hyperline customer id (cus_…) | all invoiced-in-scope |
| 2 | — (no contact entity; emails on customer) | Contact (native, sub-resource) | POST /customers/{ref}/contacts | hyp:{cus_id}:{email} (synthetic) | ~1,961 projected |
| 3 | Invoice (type=="invoice") | Invoice (native) | POST /invoices (upsert) | externalId = inv_… · customId = trimmed number | 3,230 · 589 open |
| 4 | Transaction (embedded in invoice) | Payment (native) | POST /payments + allocation | externalId = tra_… | 2,506 settled |
| 5 | Credit note (type=="credit_note") | Credit Note (native) | POST /credit_notes + allocation | externalId = Hyperline id | 277 (274 linked) |
| 6 | Invoice PDF (authenticated download) | Invoice PDF (Upload API) | POST /invoices/external:{id}/pdf | invoice externalId | 1 per synced invoice |
| 7 | Refund signal (refunded_at) | Refund (native) | not fed in v1 — see § 9 | — | 0 money-out refunds |
state and amountOutstanding are not writable. An invoice becomes PAID only through payment / credit-note allocations covering its amount. The payments flow is therefore the mechanical carrier of invoice status — reconciliation itself stays in Hyperline.POST /payments is rejected on an Upflow org connected to a native billing integration. The three Kolsquare orgs must remain API-fed only (this connector is the feed).4. Customers
- Creation ownership: the connector creates customers (Hyperline = source of truth for customer creation, per Upflow's written guidance, 17 Jul).
- Inclusion rule: a customer is created in Upflow iff it owns ≥1 invoice inside the import scope. Ongoing: a customer enters at its first finalized (non-draft) invoice. Never-invoiced customers never enter (773 no-invoice records excluded as noise).
| Hyperline field | → Upflow field | Notes |
|---|---|---|
id (cus_…) | externalId | The matching key (settled 17 Jul): 100% coverage, unique, immutable, single creator. |
name | name | Direct. |
billing_address {…} | address {…} | Field-by-field mapping. |
vat_number | vatNumber | Direct. |
billing_email, invoice_emails[] | → Contact flow (§ 5) | Emails live only on contacts in Upflow. |
Salesforce link (integrations[].entity_id) | Custom field salesforce_account_id | Carried where present. Not the matching key (§ 13). Feeds the SF-link coverage report. |
invoicing_entity_id | org routing (×3) | Drives which Upflow org receives the customer and all attached objects. |
status, deleted_at | sync filter | Only active customers in scope; archived/deleted → surfaced in the run report. |
currency, language, timezone, subscriptions, payment methods, price book, segments, domain, tax ids | not mapped | Billing-engine internals, no A/R equivalent. |
Upflow native fields not fed by this connector: accountManagerId, assignedUsers, dunningPlanId (Upflow configuration), parent, paymentMethods, accountingRef.
5. Contacts
invoice_emails[] when set; billing_email is the fallback only. The email snapshot embedded in invoices (invoice.customer.email) is frozen at emission and is never read.- Derivation: per synced customer, deduplicated union (trim + lowercase) of
billing_email+invoice_emails[]→ one Upflow contact per email, create-if-absent. - Primary recipient (
isMain): firstinvoice_emails[]entry when the list is set, elsebilling_email— faithful to Hyperline routing. - No name fabrication: first/last names left empty. Person names may come later from Salesforce enrichment (out of scope).
- Ownership boundary: the connector only ever touches contacts with the
hyp:externalId prefix. A billing box removed in Hyperline gets itshyp:contact removed; contacts from any other source (manual, Salesforce) are never touched. - Duplicate-email safety: settled in writing (17 Jul): Upflow sends each email only once even with duplicated contacts — no double-dunning risk.
Source configurations (invoiced cohort, n = 1,429, drafts excluded):
| Configuration | Volume | Contacts created | Primary |
|---|---|---|---|
billing_email only | 555 (39%) | 1 | billing_email |
invoice_emails only | 229 (16%) | 1 per list entry | first list entry |
| both set | 628 (44%) | deduplicated union | first invoice_emails entry; billing_email becomes secondary |
≥2 invoice_emails (subset) | 328 (23%) | 1 per box | first list entry (deterministic projection of Hyperline's send-to-all) |
| no email at all | 17 (1%) | none — flagged in quality report | — (customer still created) |
contacts field of the customer import: that path has documented full-replace semantics (any omitted contact is deleted) and would destroy contacts created by other writers. Only the per-contact sub-resource endpoint is used.Dunning recipient policy (OPEN — Upflow onboarding decision, not connector behaviour): "all contacts" is the only policy reproducing Hyperline's multi-box sending (328 customers have ≥2 recipient boxes); "main only" under-sends for those. Every contact the connector creates is a billing box by construction. Data provided; decision owned by Upflow/Kolsquare at onboarding.
6. Invoices
| Hyperline field | → Upflow field | Notes |
|---|---|---|
id (inv_…) | externalId | Stable upsert key. |
number | customId | Trimmed (leading space present in the API payload). Human cross-system key. |
emitted_at / due_at | issuedAt / dueDate | Upflow derives DUE vs OVERDUE. |
currency, total_amount, amount_excluding_tax | currency, grossAmount, netAmount | Cents → cents, currency as-is (no conversion). |
customer.id | customer: {externalId} | Same pivot key as § 4. |
status, amount_due, amount_paid | never pushed | Upflow state is derived from allocations (§ 3 key fact). Used for run reports and consistency checks only. |
reference / purchase_order / service period / public_url | custom fields (final list at build) | Upflow purchaseOrder is not API-writable → custom field. |
| PDF binary | POST …/pdf | Authenticated download → upload relay. Re-pushed on every touched invoice (last-write-wins). |
| tax details, discounts, FX fields, line items, subscription/payment-method/bank refs | not mapped | No A/R equivalent. |
Status handling:
| Hyperline status | Count | Connector behaviour |
|---|---|---|
draft | 12 | Never synced. |
to_pay | 574 | Pushed; Upflow derives DUE/OVERDUE. |
partially_paid | 15 | Pushed + partial allocations → correct outstanding in reminders. |
paid | 2,642 | Pushed + full allocations (backfill: ~1 year of settled history per agreed scope). |
voided | 263 | Skipped at backfill together with the voiding credit note (each voided invoice has exactly one). Voided after sync: deleted from Upflow with its credit note and payments in the same run ("full cancellation", settled 17 Jul). |
uncollectible | 1 | OPEN Upflow WRITTEN_OFF not API-writable → surfaced in the quality report for manual write-off in the Upflow UI (Upflow/merchant action). |
error | 1 | Excluded + surfaced in quality report. |
7. Payments
Only status == "settled" transactions become payments (2,506 of 2,526; failed 15 / scheduled 5 ignored).
| Hyperline (transaction) | → Upflow (payment) | Notes |
|---|---|---|
id (tra_…) | externalId + refNumber | Machine upsert key + human-visible reference. |
amount, currency | amount, currency | Cents → cents. |
settled_at | validatedAt | Day-granularity in Hyperline (measured); reflects bank date, recording arrives at reconciliation. |
payment_method_type | instrument | transfer (2,022) → WIRE_TRANSFER · direct_debit (21) → DIRECT_DEBIT · external (483) → OPEN UNKNOWN vs WIRE_TRANSFER, awaiting Kolsquare. |
| parent invoice | linkedInvoices[] allocation | The allocation is what flips invoice status. |
status | state: VALIDATED |
8. Credit notes
Hyperline stores credit notes in the same collection as invoices (type == "credit_note", amounts positive, 274/277 linked to an origin invoice). Upflow has a dedicated collection.
Field mapping mirrors invoices (externalId, trimmed number → customId, cents, customer pivot, original_invoice_id → linkedInvoices[] allocation, PDF relay). Sync rule by origin-invoice status (measured; no invoice carries more than one credit note):
| Origin invoice status | Count | Rule |
|---|---|---|
voided | 263 | Skip the pair — pure annulation, no A/R value (consistent with § 6). |
open (to_pay / partially_paid) | 4 | v1-critical: push + allocate. The credit reduces the outstanding; without it Upflow over-dunns. |
paid / uncollectible | 7 | Pushed within history scope. OPEN build detail: allocate vs leave as unapplied customer credit. |
| no origin invoice | 3 | Quality report. |
9. Refunds
- Measured (16 Jul): all 175 invoices carrying
refunded_atare targets of a credit note; zero transactions were ever refunded. In Hyperline as Kolsquare uses it, "refunding" = issuing a credit note (accounting reversal), not money sent back. - Consequence: the substance is fully carried by the credit-note flow (§ 8). Upflow's
/refundscollection has nothing to carry → not fed in v1. - Guard: if a sync run ever encounters a refunded transaction → fail-loud alert + manual review; the refunds mapping gets wired then, against real cases.
10. Data quality & run reporting
Every run produces a report. Exclusions and anomalies are always surfaced, never silent:
- Customers with zero email (17 today) — customer still created, flagged.
- Salesforce-link coverage (informational): invoiced customers without a SF link (~120), duplicated SF Account IDs (28).
- Invoices in
error(1) /uncollectible(1). - Credit notes without an origin invoice (3).
- Synthetic payments generated for marked-as-paid invoices (4 today).
- Customers on an unknown invoicing entity (0 expected — hard flag).
- Any refunded transaction (0 expected — hard alert).
- Records deleted in Hyperline detected by the nightly sweep.
- Invoices whose PDF could not be retrieved from Hyperline — the invoice data still syncs, the PDF is retried and the failure is flagged.
11. Scope: in / out
IN SCOPE — v1
- Customers, contacts, invoices, invoice & credit-note PDFs, payments, credit notes (as specified §§ 4–8)
- Routing to 3 Upflow orgs by invoicing entity
- Initial import (open all-age + ~1 year settled + complete chains on open)
- Hourly incremental sync + pre-dunning run + nightly sweep (incl. deletion detection)
- Voided-after-sync cancellation handling
- Synthetic payments for marked-as-paid invoices
- Dry-run mode + per-run quality reporting + alerting with PS visibility
- Hosting, monitoring and operation on Reviaflow EU infrastructure
- Runbook + architecture documentation, handover-ready codebase
OUT OF SCOPE — v1
- Real-time webhook layer (endpoint reserved at infra level; available as a later evolution)
- Refunds flow (zero real cases — runtime guard only, § 9)
- Anything related to the Salesforce connection: its matching, dedup, contact enrichment, or fixes to Kolsquare's SF→Hyperline feed (§ 13)
- Dunning plans, reminder policies, recipient policy — Upflow onboarding configuration
- Hyperline-side configuration (native reminders toggle, reconciliation cadence) — merchant-side
- Draft invoices (never synced)
- Historical settled invoices older than ~1 year (unless chained to an open invoice)
- Kolsquare data cleanup (duplicate SF IDs, missing emails) — reported, not fixed at source
- Additional merchants — separate onboarding, quoted per merchant
12. Open items
Non-blocking for the build; each lands before its build phase:
- OPEN
uncollectibletransitions post-sync (1 today): WRITTEN_OFF not API-writable → flagged in the quality report for manual write-off in the Upflow UI (Upflow/merchant action). - OPEN
externalpayment-method semantics (483 transactions) → instrument mapping, awaiting Kolsquare. - OPEN Final list of optional custom fields (service period, public URL, PO/reference) — definitions provisioned ×3 orgs.
- OPEN
isMainvs manual curation in Upflow — build detail. - OPEN Credit on an already-settled invoice: allocate vs unapplied credit — build detail.
- OPEN Dunning recipient policy (all vs main) — Upflow/Kolsquare onboarding decision (data in § 5).
13. Decision record
Salesforce is out of connector scope (settled 17 Jul, confirmed in writing). Upflow's guidance: Hyperline is the source of truth for customer creation; the SFDC connection is secondary, optional enrichment ("I wouldn't worry about SFDC here"). Decisions built on that guidance:
- Customers are keyed on the Hyperline id (
externalId = cus_…): 100% coverage, unique, immutable. The Salesforce Account ID is carried as custom fieldsalesforce_account_idwhere present. - The connector takes no responsibility for the future SFDC connection's matching or dedup behaviour. If that connection, once plugged, fails to match existing customers or creates duplicates, resolution belongs to that project (Upflow onboarding + Kolsquare), not to this connector.
- What the connector still provides for that future wiring:
salesforce_account_idon every linked customer + an informational SF-link coverage report at every run.
Sign-off trail: ownership, org topology, voided handling, import scope — Sarah-Nicole in writing, 17 Jul 2026 · customer key framing — explicit confirmation ask, confirmed same day.