API integration: how to connect your website, online store or admin panel to ERP, CRM, accounting and payments
Blog
Technology

API integration: how to connect your website, online store or admin panel to ERP, CRM, accounting and payments

Data map, webhooks, synchronisation, queues, idempotency, error handling and key security: what has to work before an integration goes into production

DualFroz - VulCode CEODualFroz - VulCode CEO·2 October 2026·24 min read

An API integration works well when, for every piece of information, you know three things: which system is its source, who sends it, and what happens when sending fails. Connecting a store to an ERP, a CRM, accounting software or a payment provider is usually just a few API calls. Most of the work, and most of the failures, concern situations you never see in a demo: an event delivered twice, a response that never arrived, and a system that happens to be in the middle of an update.

We base the technical examples on the public documentation of the payment provider Stripe, because it describes these behaviours explicitly. With other providers the details differ, but the problems are the same.

In short
  • Start with a data map: for every field, one source system, a direction of flow and an acceptable delay.
  • A webhook is a signal, not a guarantee. It can arrive twice and out of order, and its signature has to be verified.
  • There should be a queue between systems. The order is saved immediately, and sending it to the ERP happens in the background, with retries.
  • An operation that creates a document must be idempotent, otherwise a retry after a timeout creates duplicate invoices.
  • Keep API keys out of the code, separate for each environment, with minimal permissions and a rotation plan.
  • An integration without monitoring and a discrepancy report breaks silently.

Data map: where to start an API integration with ERP and CRM

The first deliverable of an integration project is not code, it is a table. For each type of data you write down the system that is the source of truth, the direction of flow, and the moment the data has to reach the other side. For a store connected to an ERP, a CRM, accounting software and a payment provider, it could look like this:

Example data map

DataSource of truthDirection and timing
Product master data: name, code, VAT rateERPFrom the ERP to the store, after every change or on a schedule
Marketing descriptions and photosStoreStay in the store
Stock levelsERPFrom the ERP to the store, frequently, and the store reserves units when an order is placed
PricesERPFrom the ERP to the store, together with the date they take effect
OrdersStoreFrom the store to the ERP, after payment is confirmed
Payment statusPayment providerFrom the provider to the store via webhook
InvoicesERP or accounting softwareThe number and the document for the customer come back to the store
Form enquiriesWebsiteFrom the website to the CRM, immediately
Fulfilment status and tracking numberERP or courier systemTo the store, and from there a notification to the customer

The most important rule of the map: one source of truth per field, not per record. The same customer exists in every system, but the customer changes their delivery address in their store account, trading terms are set in the ERP, and the account manager is assigned in the CRM. If two systems can edit the same field, you have to agree a conflict resolution rule in advance. The default "last write wins" rule silently wipes out changes someone made in the other system a few minutes earlier.

The second element of the map is the acceptable delay. A product description can appear in the store an hour later, the stock level of a fast-selling item should be up to date within minutes, and the payment status within seconds. That number determines the choice of data exchange technique, and so the cost of the integration.

The third element is the ID mapping table. An order has one number in the store, another in the ERP and yet another at the payment provider. The integration keeps these links in one place, because without them you cannot answer a simple question: which invoice belongs to which order.

Webhook, API polling or file exchange

How you exchange data depends on what the system on the other side can do and on the acceptable delay from the data map.

Ways to exchange data

MethodWhen it fitsWhat to watch out for
Webhook, meaning the source system sends the event to your URL itselfPayments, status changes, anything you need to react to quicklyDuplicates, event order, a public URL and signature verification
Polling the API at a fixed intervalThe system does not send webhooks, or the data changes at a predictable paceRate limits, a delay equal to the interval, a "changed since" filter is needed
Exchanging CSV or XML filesOlder systems, large batches of data processed overnightNo confirmation for individual records, date formats and character encoding
An agent installed next to the systemAn ERP running on a server in the office, with no API reachable from the internetAnother component to update and monitor

The last row applies to companies where the sales and inventory system runs on-premises. An agent is a small program on the same server that reads and writes data in the ERP through the interface the vendor provides, and connects to the internet itself, through an outbound connection. That way you do not have to open ports to the office server. Before you get a quote, ask the ERP vendor which programming interface it offers and whether access to it requires an extra licence or module.

With file exchange, the trouble is usually the format, not the exchange itself. Dates written in different ways, a comma or a full stop in amounts, a character encoding other than UTF-8 that turns Polish characters into garbage. A file import should check these things before saving and reject the whole file with a readable report, instead of loading half of it.

In practice the methods are combined. A webhook gives you a fast signal, and periodic reconciliation, for example every night, is a safety net that catches everything the signal missed.

Webhooks: what to handle before the first payment arrives

Stripe's documentation on receiving webhooks shows well what to expect from any event sender. Stripe notes that the same event may occasionally be delivered more than once, that it does not guarantee delivery of events in the order they were generated, and that when delivery fails it retries in live mode for up to three days with exponential backoff. It also recommends that the endpoint quickly return a 2xx status code before running any complex logic, for example marking an invoice as paid in the accounting system.

These behaviours lead to a mandatory list for every webhook receiver:

1
Verify the signature

The sender signs the payload with a shared secret, for example using HMAC with SHA-256, as Stripe does. The signature is computed from the raw request body, before the framework parses it, and compared using a function that is resistant to timing attacks.

2
Reject old events

A timestamp in the signed payload protects against replaying an intercepted request. Stripe's libraries allow a 5-minute difference by default.

3
Store the event ID

An event with a known ID is acknowledged with a 200 code, and nothing is done a second time.

4
Respond quickly, process in the background

The receiver only saves the event and a job to be done. The actual work happens in the queue.

5
Do not rely on order

If the "paid" event arrives before "created", fetch the current state of the object from the API instead of rebuilding it from the event history.

6
Reconcile state periodically

The sender's retries end at some point. An outage longer than the retry window will only be caught by comparing state on both sides.

The example below for Node.js and Express 5 combines steps one to four. The header names are placeholders, because every sender has its own. The event and the job are saved in a single database transaction, so there is no state in which the event is saved but the job is missing.

webhook.ts · ts
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
import { Pool } from "pg";

const app = express();
const db = new Pool(); // connection details from PGHOST, PGUSER, PGPASSWORD, PGDATABASE
const secret = process.env.WEBHOOK_SECRET ?? "";
const toleranceSec = 300;

app.post("/webhooks/payments", express.raw({ type: "*/*" }), async (req, res) => {
  const body = req.body.toString("utf8");
  const timestamp = req.header("x-webhook-timestamp") ?? "";
  const signature = Buffer.from(req.header("x-webhook-signature") ?? "", "hex");
  const expected = createHmac("sha256", secret).update(`${timestamp}.${body}`).digest();
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= toleranceSec;

  if (signature.length !== expected.length || !timingSafeEqual(signature, expected) || !fresh) {
    res.sendStatus(400);
    return;
  }

  const event = JSON.parse(body);
  const client = await db.connect();
  try {
    await client.query("BEGIN");
    const saved = await client.query(
      "INSERT INTO webhook_events (event_id, type, payload) VALUES ($1, $2, $3) ON CONFLICT (event_id) DO NOTHING",
      [event.id, event.type, event],
    );
    if (saved.rowCount === 1) {
      await client.query("INSERT INTO jobs (kind, ref_id) VALUES ('payment_event', $1)", [event.id]);
    }
    await client.query("COMMIT");
  } catch (err) {
    await client.query("ROLLBACK");
    throw err;
  } finally {
    client.release();
  }
  res.sendStatus(200);
});

app.listen(3000);

The event_id column in the webhook_events table has a unique constraint. We ran the example on Node.js 22, Express 5 and PostgreSQL 17: a repeated event leaves one row in each table, a bad signature or an old timestamp returns 400, and when the database is unavailable the sender gets a 500 and retries later. That is how it should be: an error on our side should result in a retry, not a lost event.

Two things you cannot see in the code. Redirecting the customer's browser to a "thank you for your payment" page is not confirmation of payment, because the customer can close the tab before returning, and the return URL can be opened by hand. The order status is changed only by an event from the provider or by polling its API. Second: the signature verification secret also gets rotated. When rotating, Stripe lets you keep the old secret active for up to 24 hours, so you have time to deploy the new one.

A queue between systems: the order does not wait for the ERP

The simplest integration sends the order to the ERP the moment the customer clicks "place order". It works as long as the ERP responds quickly. When the ERP is being updated or responds slowly, the customer sees an error or a spinning wheel, and the order may be saved in the store with no counterpart in the ERP.

The solution is a queue. The store saves the order and, in the same transaction, adds a "send to ERP" job. A separate process picks up the jobs and sends them on. When the ERP does not respond, the job waits and is retried, and the customer knows nothing about it. This pattern is sometimes called the transactional outbox, and it is exactly what the example in the previous section does on the receiving side.

Retries need increasing intervals and a small random jitter, so that after an outage hundreds of jobs do not hit the ERP in the same second. They also need a limit. A job that has used up its attempts goes onto an error list in the panel, with a description a human can understand and a "retry" button for after the cause has been fixed.

Not every error deserves a retry. The split usually looks like this:

Types of integration errors

Error typeExampleWhat to do
TransientTimeout, a 502 or 503 responseRetry with an increasing interval
Rate limitA 429 Too Many Requests responseWait, and if the server sent a Retry-After header, wait as long as it says
Data errorNo product with that code in the ERP, an invalid NIP (Polish tax ID), a 400 or 422 responseDo not retry, pass it to a person with a description
AuthorisationA 401 response after a token has expiredRefresh the token once, raise an alert on the next failure
Unknown outcomeConnection dropped after the request was sentCheck the state in the target system, or retry with an idempotency key

The 429 code and the option to include a Retry-After header with it are described in RFC 6585. Public APIs usually have rate limits, which make themselves felt at the worst moment: during the first import of the whole product catalogue, or at the end of the month when billing processes kick in. A full synchronisation should pace itself.

Idempotency: retries without duplicate invoices

An operation is idempotent if performing it several times has the same effect as performing it once. Setting a status to "shipped" is idempotent. Creating an invoice is not: every call creates a new document.

A typical scenario: the integration sends the ERP a request to create an invoice, the ERP creates it, but the response does not arrive because the connection dropped. The integration sees a timeout and retries the request. The ERP now has two invoices for one order, and someone in accounting will have to sort them out.

There are three ways to avoid this, and a good integration uses as many as the system on the other side allows.

An idempotency key. Some APIs accept a unique key in the request and, when a request is repeated with the same key, return the result of the first call instead of performing the operation again. Stripe describes this in detail: it saves the status code and body of the response to the first request with a given key, even if it was a 500 error, compares the parameters of subsequent requests with the original ones and returns an error if they differ, and keys may be removed after at least 24 hours. It also recommends random keys, for example version 4 UUIDs, with no personal data in them. The Idempotency-Key header was described in an IETF draft standard, but the draft expired in 2026 without being published as an RFC. So every API has its own rules: whether it supports keys at all, how long it remembers them and what it returns on a repeat.

Search first, then create. If the API does not support keys, the integration saves the store's order number in the document field intended for an external reference, and checks whether such a document already exists before creating one. For this to work, jobs concerning the same order must not run in parallel, because two processes can both fail to find the document at the same time and both create it.

Deduplication on the receiving side. This is what the webhook example does: the event ID saved with a unique constraint. The same technique works for file imports, where every row has an ID from the source system.

Amounts are a separate matter. Money in an integration is stored as integers in the smallest currency unit (grosze, for the Polish złoty) or in a decimal type, never as floating-point numbers, because they cannot represent most decimal fractions exactly. On top of that, the store and the ERP may calculate VAT using different methods, for example from net prices or from gross prices, or round on each line instead of on the whole document. The invoice total then differs from the order amount by a grosz or two, and payment reconciliation starts reporting discrepancies. The calculation method has to be agreed at the data map stage.

Data synchronisation: stock, prices, deletions and conflicts

Incremental synchronisation fetches only records changed since the last run. It is fast and saves rate limits, but it has two weaknesses. The first is the time boundary: a record changed during a run, or saved with a delayed timestamp, can fall between two windows. So the next run starts a little earlier than the previous one ended, and records fetched twice are handled idempotently. The second weakness is deletions. If the API does not report deleted records, incremental synchronisation will not see them, and a product withdrawn in the ERP will stay in the store.

Both weaknesses are patched by periodic full reconciliation. Once a day the integration compares the lists of IDs, the states and the checksums on both sides and produces a discrepancy report. A report that shows zero every day is the best evidence that the integration works, and discrepancies above an agreed threshold should send a notification, because nobody reads a report every day.

Stock levels need special attention, because between two synchronisation runs the same product can sell in the online store and in a physical shop. Three things help: reserving units when the order is placed, more frequent synchronisation for low-stock products, and a safety buffer, meaning the store shows a few units fewer than there are in the warehouse. The size of the buffer is a business decision, not a technical one.

Always send dates with a time zone, in ISO 8601 format with the offset from UTC. A date saved without a time zone works correctly for most of the year and breaks twice a year, when the clocks change, and also when one of the servers is in a different time zone from the other.

Polish specifics: KSeF, the VAT whitelist and REGON registry data

Accounting integrations in Poland in 2026 do not end with the accounting software. According to the Ministry of Finance timeline, issuing invoices in KSeF, Poland's National e-Invoicing System (Krajowy System e-Faktur), has been mandatory since 1 February 2026 for businesses whose sales in 2024 exceeded PLN 200 million including tax, and since 1 April 2026 for everyone else. Until the end of 2026, taxpayers whose total sales documented with invoices do not exceed PLN 10,000 including tax per month can still issue invoices outside KSeF. Receiving invoices through KSeF has been mandatory since 1 February 2026. Invoices for consumers do not have to go through KSeF, although they can.

For an integration, this means a sales invoice has its own number assigned by KSeF, and that number is worth storing with the order, while purchase invoices can come into the company's system straight from KSeF instead of through a mailbox. We described how to approach the KSeF connection itself in our KSeF integration guide.

For business customers and suppliers, two public data sources come in handy:

  • The VAT taxpayer register API, known as the VAT whitelist (Wykaz podatników VAT). It lets you check a company's VAT taxpayer status and bank account number by NIP or REGON. According to the Ministry of Finance, the "search" method allows 100 queries a day for up to 30 entities at a time, and the "check" method allows 5,000 entities to be checked. Once the limit is used up, access may be blocked until midnight. So it is worth storing the results and not checking the same counterparty with every order.
  • The REGON API run by GUS, Statistics Poland. It lets you look up company details by REGON (the national business register number), NIP or KRS (National Court Register) number, for example to fill in a business customer's registration form after they enter their NIP. According to the GUS API portal, the service and the data are free, and commercial entities receive a production key after applying to GUS.

Securing API keys and tokens

An API key for the ERP or the payment provider gives access to the company's data and money, so it is treated like an administrator password. In practice this comes down to a few rules:

  • Outside the code and the repository. Keys go into environment variables or a secrets manager. A key once committed to a repository is considered exposed, even after it is removed, because it remains in the change history.
  • Never in the browser or a mobile app. Everything that goes into frontend code is public. Calls that need a secret key go through the server, and only a key the provider explicitly intends for public use may run in the browser.
  • Separate keys for environments and integrations. A test key does not work in production, and a leaked key for one integration does not open the others.
  • Minimal permissions. If an integration only reads stock levels, its key should not be able to create documents.
  • A rotation plan. A written procedure: who rotates the key, where it gets replaced and how to check that everything works. You write it before you need it.
  • Clean logs. Integration logs contain no keys, tokens or full personal data. IDs and error codes are enough for diagnosis.

Many CRMs and cloud systems use the OAuth 2.0 protocol instead of a fixed key. The integration then receives a short-lived access token and a refresh token, which has to be stored securely. Current recommendations are collected in RFC 9700 from January 2025. Among other things, it says you must not use the flow in which the application collects the user's login and password, and that access tokens should be restricted to a specific resource server. In day-to-day work, something else matters most: the refresh token can stop working, for example when someone in the CRM revokes the integration's access. The integration must then raise an alert, not silently stop synchronising.

Integrations also carry customers' personal data. If the CRM provider, or the company that builds and maintains the integration, processes that data on your behalf, you need a data processing agreement under Article 28 GDPR, and the security measures must be appropriate to the risk, as Article 32 requires. Then there is the data minimisation principle: you send to the CRM only the fields the salesperson actually needs, not the whole order with the address and payment history.

Testing and accepting an integration

You do not test an integration with one successful order. You test it with failure scenarios, because those are what will happen in production. Before acceptance, it is worth going through a list like this in the payment provider's test environment and on a test copy of the ERP database:

1
Duplicate event

The same webhook sent twice creates one job and one document.

2
Reversed order

A "paid" event before "created" produces the correct final state.

3
Target system unavailable

With the ERP switched off for an hour, orders wait in the queue and go through without duplicates once it is back.

4
Timeout after sending

A retry does not create a second invoice.

5
Rate limit

429 responses slow the integration down but do not stop it.

6
Bad data

An order with a product that has no counterpart in the ERP goes onto the error list with a readable description.

7
Expired access

Revoking permissions in the CRM raises an alert within the agreed time.

8
Discrepancy report

A record changed by hand on one side shows up in the report after the next reconciliation.

After acceptance, the integration needs monitoring that tells you about the integration, not just about the server. Four numbers are enough to start with: the queue length, the age of the oldest pending job, the number of errors in the last hour, and the time of the last successful synchronisation in each direction. We described broader principles for observing a system after launch in our article on monitoring and maintenance after launch.

Going live in production usually starts with an initial import, and only then is the ongoing exchange switched on. The order matters: if you switch on webhooks first and do the import later, some events will concern records that do not exist yet.

How to prepare an integration enquiry

What speeds up an integration quote most is concrete information about the systems on both sides. Prepare:

  • The names and versions of the systems, and whether they run in the cloud or on a server in the company.
  • Access to the API documentation and to a test environment, if the vendor provides one, and whether the API requires an extra licence.
  • The data map from this article, even as a rough draft: what flows where, who is the source of truth and what delay is acceptable.
  • Volumes, meaning the number of orders a day, products in the catalogue and customers in the CRM, as well as peaks, for example the run-up to Christmas.
  • A person for errors, meaning who in the company will handle the list of jobs that did not go through and fix the source data.

If the data that is meant to flow between systems lives in a spreadsheet today, start by putting it in order. We described how to do that, and when a spreadsheet is worth replacing with a panel, in our article admin panel instead of a spreadsheet. When the integration is part of a new product, a file export is often enough in the first version, and the full integration comes once it is clear the product is needed, as we explain in how to build an MVP.

Frequently asked questions about API integrations

How much does an API integration cost?

It depends on the number of systems, the directions of flow and the quality of the API on the other side. One-way sending of form enquiries to a CRM is a different project from two-way synchronisation of stock, prices and orders with an ERP installed in the office. Only a data map and the API documentation give you a reliable quote, which is why we ask for the specific names of the systems, not a general "integration with accounting".

Can a store be integrated with an ERP installed on a server in the office?

Yes, usually through an agent installed next to the ERP, which connects to the store's server itself. The condition is a programming interface provided by the ERP vendor. If there is none, file exchange remains, as long as the system can export and import files.

Which is better: a webhook or API polling?

Most often, both at once. A webhook gives a fast reaction, and polling or nightly reconciliation catches events that did not arrive. If the system does not send webhooks, you are left with polling using a "changed since" filter.

How quickly will data appear in the other system?

As quickly as you set in the data map. Webhooks arrive shortly after the event, and data exchanged through polling arrives with a delay equal to the interval between requests. A shorter interval means using up rate limits faster.

What happens when the vendor changes the API?

Vendors usually version their APIs and announce when older versions will be retired. Register the developer account to a company address that someone actually reads, and specify the API version explicitly in the integration, if the vendor allows it.

Is an off-the-shelf plugin or integration platform enough?

Often, yes, especially for popular pairs of systems and standard flows. Before choosing, check whether the plugin supports your data map, including custom fields, how it reports errors, whether it has retries and how much it costs at your volume. A custom integration makes sense when the flow is unusual or when the ready-made tool gives you no control over errors.

Where to start

Start with a data map: list the information you copy by hand between systems today, which system should be its source and how quickly it has to reach the other side. That table alone will show whether you need a full integration, or whether a file export once a day is enough.

Integrations and automation are one of our services. We describe the scope on the Integrations & Automation page, and how we work on the process page. After handover you get full rights to the code and documentation, so the integration can then be maintained by your own team or another contractor. If you already have a list of systems, get in touch.