Panoramica della soluzione
Pleaxy si collega a QuickBooks Online e Xero senza configurazioni aggiuntive. I team finance più grandi però usano di solito altro: NetSuite, SAP S/4HANA, Microsoft Dynamics 365, Oracle Fusion, Sage Intacct o Workday. Non serve aspettare un connettore dedicato per usare Pleaxy con questi sistemi. L’API e i webhook di Pleaxy ti danno tutto ciò che serve per costruire da solo il giro completo, con tre piccoli pezzi di codice.
In questo articolo ti mostro come collegare Pleaxy a NetSuite, passo dopo passo: le fatture approvate diventano fatture passive, e i pagamenti che la tua contabilità fornitori effettua in NetSuite tornano in Pleaxy. Poi ti mostro cosa cambia per gli altri ERP e cosa tenere presente prima di andare in produzione.
Pleaxy
Una fattura viene confrontata con il suo ordine d’acquisto e il suo contratto, e le persone della sua catena di approvazione la approvano.
Il tuo connettore
Un webhook segnala che la fattura è cambiata. Il tuo connettore la legge e crea una fattura passiva (vendor bill) in NetSuite.
NetSuite
La contabilità fornitori paga la fattura passiva in NetSuite, come paga tutte le altre.
Pleaxy
Il tuo connettore registra il pagamento in Pleaxy. La fattura risulta pagata e il fornitore riceve la notifica di pagamento e l’avviso di pagamento.
Niente viene digitato due volte e ogni sistema mantiene il suo ruolo: Pleaxy decide cosa va pagato, NetSuite lo registra e lo paga.
Prerequisiti
In Pleaxy (un amministratore del workspace acquirente)
- Il tuo ID del workspace, da Profilo aziendale, e una chiave API per leggere le fatture.
- Una seconda chiave API con l’ambito
payments:record, creata con il ruolo di amministratore o finance. Solo una chiave con questo ambito può registrare pagamenti, quindi tienila separata dalla prima. - Un endpoint webhook: Profilo acquirente, scheda Integrazioni, Webhook, Aggiungi endpoint. Scegli l’evento
invoice.updatede copia il segreto di firma: viene mostrato una sola volta.
In NetSuite
- I servizi web REST e l’autenticazione basata su token abilitati, un record di integrazione e un token di accesso per un ruolo che può creare fatture passive ed eseguire query SuiteQL sulle transazioni.
- Una corrispondenza tra ogni fornitore Pleaxy e il suo vendor in NetSuite, con i conti di costo e di imposta su cui vengono registrate le sue fatture. Per iniziare basta una piccola tabella nel tuo connettore.
Passo 1: ricevere le fatture approvate
Prima di tutto, due piccole funzioni di supporto: una chiama l’API di Pleaxy con una chiave API, l’altra chiama i servizi web REST di NetSuite.
// Pleaxy: your buyer workspace's API base, read with an API key.
const PLEAXY = `${process.env.PLEAXY_API_BASE_URL}/v1/workspaces/${process.env.PLEAXY_WORKSPACE_ID}`;
export async function pleaxy(path, { method = "GET", body, apiKey = process.env.PLEAXY_API_KEY } = {}) {
const res = await fetch(`${PLEAXY}${path}`, {
method,
headers: { "X-Api-Key": apiKey, "Content-Type": "application/json" },
body: body && JSON.stringify(body),
});
if (!res.ok) throw new Error(`Pleaxy ${res.status}: ${await res.text()}`);
return res.json();
}
// NetSuite REST web services. signNetSuite adds the token-based authentication
// header (OAuth 1.0a, HMAC-SHA256) with the OAuth library you already use.
const NETSUITE = `https://${process.env.NS_ACCOUNT_HOST}.suitetalk.api.netsuite.com/services/rest`;
export async function netsuite(method, path, body, headers = {}) {
const url = `${NETSUITE}${path}`;
const res = await fetch(url, {
method,
headers: { ...signNetSuite(method, url), "Content-Type": "application/json", ...headers },
body: body && JSON.stringify(body),
});
if (!res.ok) throw Object.assign(new Error(`NetSuite ${res.status}: ${await res.text()}`), { status: res.status });
return res.status === 204 ? undefined : res.json(); // a created record answers 204
}Quando una fattura viene approvata, Pleaxy invia con POST un evento invoice.updated al tuo endpoint. L’evento indica la fattura ma non la contiene, quindi una richiesta falsificata non può inserire nulla nella tua contabilità: il connettore legge la fattura da sé, con la propria chiave. Verifica comunque l’intestazione Pleaxy-Signature, rispondi entro 10 secondi e fai il lavoro dopo. Pleaxy ritenta ogni consegna che non riceve un 2xx, per circa due giorni.
import crypto from "node:crypto";
import express from "express";
import { syncInvoice } from "./vendor-bill.mjs";
const app = express();
// Pleaxy signs the raw body, so read it as text and parse it after checking.
app.post("/pleaxy/webhooks", express.text({ type: "application/json" }), (req, res) => {
if (!isFromPleaxy(req.get("Pleaxy-Signature") ?? "", req.body, process.env.PLEAXY_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
res.sendStatus(204); // answer within 10 seconds, then do the work
if (event.type === "invoice.updated") {
syncInvoice(event.documentId).catch(console.error); // in production: a job queue
}
});
// Pleaxy-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<body>">
function isFromPleaxy(header, rawBody, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
if (!parts.t || !parts.v1 || Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest();
const actual = Buffer.from(parts.v1, "hex");
return actual.length === expected.length && crypto.timingSafeEqual(actual, expected);
}
app.listen(3000);Preferisci il polling, o vuoi una rete di sicurezza per un giorno in cui il tuo endpoint era fuori servizio? GET /sync/events elenca gli stessi eventi, dal più vecchio, a partire da un cursore che conservi tu. Vedi Sincronizzazione dei documenti.
Passo 2: registrare la fattura passiva
invoice.updated scatta anche quando una fattura viene rifiutata o cambiano le sue coordinate bancarie, quindi il connettore chiede prima a che punto è la fattura e prosegue solo se è approved. Poi legge la fattura e crea la fattura passiva, con il numero di fattura del fornitore, le date, le righe, gli sconti, gli addebiti e le imposte, così il totale corrisponde all’importo approvato in Pleaxy.
import { pleaxy, netsuite } from "./clients.mjs";
// Your mapping: the NetSuite vendor for each Pleaxy supplier (supplierOrgId),
// and the accounts its spend and tax are booked to.
const VENDORS = {
"org_01k5x8q3m7c2vd9pzt4wbh6ner": { vendorId: "1842", expenseAccountId: "6120" },
};
const TAX_ACCOUNT_ID = "2310";
export async function syncInvoice(invoiceId) {
// invoice.updated also fires for rejections and bank details:
// book only an invoice that is approved and not yet paid.
const status = await pleaxy(`/invoices/${invoiceId}/status`);
if (status.status !== "approved") return;
// Events can arrive twice. The external ID makes the bill unique in NetSuite,
// so check for it first: a second delivery finds the bill and stops.
const externalId = `pleaxy-${invoiceId}`;
if (await billExists(externalId)) return;
const invoice = await pleaxy(`/invoices/${invoiceId}`);
const vendor = VENDORS[invoice.supplierOrgId];
if (!vendor) throw new Error(`No NetSuite vendor mapped for ${invoice.supplier}`);
// Lines, invoice-level discounts and charges, and tax add up to the invoice's total.
const line = (amount, memo, accountId = vendor.expenseAccountId) => ({ account: { id: accountId }, amount, memo });
const items = [
...invoice.lineItems.map((l) => line(l.amount, l.description)),
...(invoice.adjustments ?? []).map((a) => line(a.type === "discount" ? -a.amount : a.amount, a.description)),
...(invoice.taxAmount ? [line(invoice.taxAmount, "Tax", TAX_ACCOUNT_ID)] : []),
];
await netsuite("POST", "/record/v1/vendorBill", {
externalId,
entity: { id: vendor.vendorId },
tranId: status.invoiceNumber, // the supplier's invoice number
tranDate: status.issueDate,
dueDate: status.dueDate,
memo: [`Pleaxy ${invoiceId}`, invoice.po].filter(Boolean).join(" · "),
approvalStatus: { id: "2" }, // Approved: people already approved it in Pleaxy
expense: { items },
});
}
async function billExists(externalId) {
try {
await netsuite("GET", `/record/v1/vendorBill/eid:${externalId}?fields=id`);
return true;
} catch (err) {
if (err.status === 404) return false;
throw err;
}
}- Una fattura passiva per fattura. Gli eventi possono arrivare due volte. L’ID esterno della fattura passiva,
pleaxy-seguito dall’ID della fattura, è univoco in NetSuite, quindi un evento ripetuto trova la fattura passiva e si ferma. - Approvata una volta sola. La fattura passiva entra già approvata: gli approvatori hanno già deciso in Pleaxy, con la fattura confrontata con il suo ordine, il contratto e i consumi. Se il tuo NetSuite instrada le fatture passive per l’approvazione, ometti questo campo e la approveranno anche lì.
- Il PDF, se ti serve.
GET /invoices/{id}/documentrestituisce la fattura in formato UBL 2.1, e il PDF della fattura può essere allegato alla fattura passiva come file.
Passo 3: riportare indietro i pagamenti
La contabilità fornitori paga la fattura passiva in NetSuite, con assegno, ACH o un ciclo di pagamenti. Un job pianificato trova poi con una query SuiteQL le fatture passive di Pleaxy pagate per intero e le registra in Pleaxy con RecordPayments, fino a 50 alla volta.
import { pleaxy, netsuite } from "./clients.mjs";
// Bills that came from Pleaxy, now paid in full, with the bill payment that
// settled them, changed since the last run. Run it every 15 minutes or so.
const paidBills = (since) => `
SELECT DISTINCT bill.externalid AS externalid, pay.id AS paymentid, pay.tranid AS tranid,
TO_CHAR(pay.trandate, 'YYYY-MM-DD') AS paydate
FROM transaction bill
JOIN NextTransactionLineLink link ON link.previousdoc = bill.id
JOIN transaction pay ON pay.id = link.nextdoc
WHERE bill.type = 'VendBill' AND bill.status = 'B' -- B: paid in full
AND pay.type = 'VendPymt'
AND bill.externalid LIKE 'pleaxy-%'
AND pay.lastmodifieddate >= TO_DATE('${since}', 'YYYY-MM-DD HH24:MI:SS')`;
export async function syncPayments(since) {
const { items } = await netsuite("POST", "/query/v1/suiteql", { q: paidBills(since) }, { Prefer: "transient" });
// Pleaxy records one payment for the whole amount due: for a bill paid in
// instalments, send the payment that finished it.
const latest = new Map();
for (const row of items) {
if (!latest.has(row.externalid) || row.paydate >= latest.get(row.externalid).paydate) latest.set(row.externalid, row);
}
const payments = [...latest.values()].map((row) => ({
invoiceId: row.externalid.replace(/^pleaxy-/, ""),
method: "ach", // or map it from the bill payment's payment method
paymentDate: row.paydate,
reference: row.tranid || `NS-${row.paymentid}`, // the supplier sees it: never an account number
}));
// Up to 50 a call, with a key that has the payments:record scope.
// Repeating a payment is safe: it comes back as already_recorded.
for (let i = 0; i < payments.length; i += 50) {
const { results } = await pleaxy("/invoices/payment-records", {
method: "POST",
body: { payments: payments.slice(i, i + 50) },
apiKey: process.env.PLEAXY_PAYMENTS_KEY,
});
for (const r of results.filter((r) => r.outcome === "refused")) {
console.error(`${r.invoiceId}: ${r.code} ${r.message}`);
}
}
}Ogni fattura registrata risulta pagata in Pleaxy. Il fornitore riceve una notifica di pagamento e un avviso di pagamento con il tuo riferimento, e il suo sistema riceve un evento invoice.paid. Registrare un pagamento non sposta denaro: dice cosa NetSuite ha già pagato. Eseguire il job due volte non crea problemi, perché un pagamento già registrato torna come already_recorded.
Lo stesso giro per SAP, Dynamics e gli altri
La parte Pleaxy non cambia: il webhook, le due letture e RecordPayments sono uguali per ogni ERP. Cambia solo la parte ERP: quale record diventa la fattura approvata e dove il connettore scopre che è stata pagata.
- SAP S/4HANA: una fattura fornitore, creata tramite la sua API per le fatture fornitore; i pagamenti dalle partite pareggiate della contabilità fornitori.
- Microsoft Dynamics 365 Business Central: una fattura di acquisto, creata e registrata tramite la sua API; i pagamenti dai movimenti contabili fornitore.
- Microsoft Dynamics 365 Finance: una fattura fornitore, tramite le sue entità dati; i pagamenti dalle transazioni fornitore liquidate a fronte di essa.
- Oracle Fusion Cloud ERP: una fattura passiva (payables invoice), tramite la sua API REST; i pagamenti dai pagamenti dei debiti effettuati a fronte di essa.
- Sage Intacct: una fattura passiva (AP bill); i pagamenti dai pagamenti AP che l’hanno saldata.
- Workday: una fattura fornitore, tramite i suoi servizi web; i pagamenti dallo stato di pagamento della fattura fornitore.
Un ERP o un hub di fatturazione elettronica che accetta già documenti standard può saltare la mappatura: Pleaxy fornisce ogni fattura come Invoice UBL 2.1, il suo stato come StatusUpdateRequest cXML o ApplicationResponse UBL, e ogni pagamento come PaymentRemittanceRequest cXML o RemittanceAdvice UBL.
Buono a sapersi
- Le approvazioni restano alle persone. Una chiave API può leggere le fatture e registrare pagamenti, ma non può mai approvare o rifiutare una fattura: lo fa sempre la persona indicata dal passaggio di approvazione, in Pleaxy.
- I pagamenti sono per l’intero importo dovuto. Pleaxy non registra pagamenti parziali, quindi invia una fattura passiva solo quando è pagata per intero.
- Il tuo team vede cosa ha fatto la chiave. Ogni chiamata che registra pagamenti genera una notifica nel workspace con il nome della chiave e le fatture, e una chiave registra al massimo 500 pagamenti al giorno.
- I problemi di consegna sono visibili. L’elenco Consegne del webhook mostra ogni evento, i suoi tentativi e cosa ha risposto il tuo endpoint, e Invia test invia un ping firmato.
Conclusione
In questo articolo ti ho mostrato come collegare Pleaxy a NetSuite con tre piccoli pezzi di codice: un ricevitore di webhook, un job che registra le fatture approvate come fatture passive e un job che riporta indietro i pagamenti. I tuoi approvatori continuano a decidere in Pleaxy, la tua contabilità fornitori continua a pagare in NetSuite e i tuoi fornitori vengono informati di ogni pagamento senza che nessuno debba ridigitarlo.
Per iniziare, crea una chiave API e un endpoint webhook nel tuo workspace acquirente e leggi i riferimenti Webhook e Sincronizzazione dei documenti. Se usi un ERP che non trovi qui, o vuoi una mano con il tuo primo connettore, contattaci.
Informazioni sull’autore
Ryan Brown
Lead developer, piattaforma Pleaxy
Ryan Brown è il lead developer della piattaforma Pleaxy. Sviluppa l’API, i webhook e le integrazioni che collegano Pleaxy ai sistemi contabili ed ERP che i team finance già usano.