Integration framework

Connect your shop to everything around it.

An integration framework built for Magento. It moves orders, stock, prices and products between your shop and your ERP, warehouse and accounting, and shows you what happened to every record. With custom connectors, it connects anything. Your integrations live in your own git repository. Source-available, yours to keep.

Deedspan sits between your shop and your other systems. Orders and their changes go from the shop through Deedspan to the ERP and accounting. Stock, prices, the catalogue, shipments and document numbers come back from those systems into the shop.

Your shop

Magento a thin module and the standard API
Orders Stock Prices Products Categories
Magento adminexport state and a retry button on every order

Your install

deedspan

  1. Map each record to the canonical model
  2. Keep only what changed
  3. Send in chunks, retry what fails
  4. Store every record's result

Flows, mappings and hooks come from your git repository.

Your systems

ERP and accountingPohoda, Helios, Fakturoid, or any other
Warehouse and carriersstock files, shipments, tracking
PIM, feeds, CRMover REST, files, SQL or webhooks
  1. A customer places order #1042 in Magento.
  2. Deedspan maps it and sends it to the ERP.
  3. The ERP's number goes back to the order.
  4. New stock arrives from the warehouse.
  5. Nothing changed for BOOT-42, so nothing is sent.

What it does, by example

Six jobs, each one a flow

A flow moves one kind of record in one direction. These are typical ones. Each is a short YAML file you can read in a minute.

Orders to the ERP, with write-back

A paid order goes to your ERP as a sales order, over REST or through a ready connector. The ERP's document number comes back onto the Magento order, with a comment in its history. Later changes and cancellations follow.

Source a Magento order, once it is paid

000012345  processing  paid
2 lines    189.80 EUR

Result your ERP, and back on the order

ERP      sales order SO-20871   acknowledged
Magento  external ID + comment  written

Stock from the ERP every 15 minutes

One SQL query reads the rows that changed since the last run. Deedspan updates Magento's MSI sources and sends only the quantities that differ.

Source the ERP's database, over MS SQL

SELECT sku, warehouse, qty
FROM erp_stock
WHERE changed_at > :since

Result Magento MSI source items

TENT-200  main  15  written
BOOT-42   east   3  unchanged

Prices per channel, with tiers

The ERP's price file sets the base price in each store, special prices, and tier prices by customer group and quantity. Only prices that differ from what Magento holds are written.

Source prices_*.csv on the ERP's SFTP

sku;channel;price;currency
TENT-200;eu;249.00;EUR
TENT-200;uk;219.00;GBP

Result Magento base prices per store

TENT-200  eu_store  249.00  written
TENT-200  uk_store  219.00  written

The whole catalogue

Simple and configurable products, categories and related products. Missing ones are created, existing ones updated. If you switch it on, products the source dropped are deleted, up to a limit you set.

Source products_*.csv from the ERP or PIM

sku,type,size,variants
BOOT,configurable,,BOOT-42;BOOT-43
BOOT-42,simple,42,
BOOT-43,simple,43,

Result Magento products

BOOT-42  simple, size 42        written
BOOT-43  simple, size 43        written
BOOT     configurable, 2 sizes  written

Invoices in Fakturoid

Each order becomes an invoice through Fakturoid's API. A change edits the draft. Once the invoice is issued, a change makes a correction document and a cancellation makes a credit note.

Source the order and what happens to it later

000012345  created
000012345  cancelled

Result Fakturoid documents

invoice      2026-0412  acknowledged
credit note  2026-0431  acknowledged

Shipments and tracking, back to the shop

The warehouse reports what it shipped. The order in Magento gets its shipment and tracking number. Its status stays as it is unless the flow says otherwise.

Source shipments_*.csv from the warehouse

order;carrier;tracking
000012345;DHL;TRK-40950012

Result the Magento order

000012345  shipment + tracking  written

A framework, not a fixed integration

Anything to anything, through one model

Deedspan does not connect system A to system B. Every system connects to one canonical model. Add a connector once, and every flow can use it with every other system.

Shops

Magento WooCommerce PrestaShop Sylius

ERP and accounting

Pohoda Helios Fakturoid any ERP over REST, SQL or files

The canonical model

Shop-neutral records with your own fields and your own record types.

order order_update product category price stock_level + yours

Warehouses and carriers

stock, shipments, tracking

PIM and feeds

product data in, feeds out

CRM and marketing

customers and orders, pushed or pulled

A connection is three parts you combine

Pick a transport and a format, or a ready connector for a known system. The core is complete on its own: a connector saves work, it never unlocks a capability.

How data travels

Transports

REST over HTTP SOAP SFTP FTP MS SQL other databases webhooks e-mail S3

What it looks like

Formats

CSV, any encoding XML JSON JSON lines fixed-width database rows

Who is on the other end

System connectors

Magento, in the core Pohoda Helios Fakturoid your own

Magento, out of the box

Deep Magento support is in the core: a thin module, screens in Magento admin and the standard API, for orders, stock, prices and the catalogue.

Anything else, with a connector

For any other system, write a connector: a YAML manifest over the generic transports and formats, with PHP only where the manifest is not enough. It lives in your project's connectors/ folder or ships as a package, and every flow can use it.

Three levels of customisation

Start with the simplest one and go deeper only where a merchant's rules need it.

  1. YAML mapping

    Field mappings, lookup tables and defaults. Enough for most flows.

    Nothing executes.
  2. Sandboxed expressions

    One expression per field, from a fixed list of functions: trim(), lookup(), decimal(), split() and more.

    Cannot reach files, the network or the database.
  3. PHP hooks

    A class in your project that changes, splits or skips a record: after reading, after mapping, before sending, or on an error.

    Your own code, in your own repository.

A flow is a file you own

One integration, one YAML file

This is a complete stock flow. It sits in your git repository next to its lookup tables and sample files, and goes through code review like any other change.

flows/stock-from-erp.yaml

flow: stock-from-erp
owner: erp
record: stock_level
1triggers:
  schedule: "*/15 * * * *"
2source:
  transport: sftp
  connection: erp-ftp
  path: /export/stock_*.csv
  format: { type: csv, delimiter: ";" }
3map:
  sku:      "trim(row['sku'])"
  location: "lookup('warehouses', row['warehouse'])"
  quantity: "max(0, int(row['qty']))"
4hooks:
  before_send: Acme\Hooks\MergeConsignmentStock
5target:
  connector: magento
  operation: stock.update
  connection: shop
  locations: { main: default, east: east }
6rules:
  unknown_references: fail
  max_change_share: 0.3
  1. 1

    Triggers

    A cron schedule, a signal from the shop that something changed, a webhook from another system, or a manual run. A flow never runs twice at once.

  2. 2

    Source

    A transport and a format. Here a CSV file on SFTP. It could be a REST call, an SQL query or a stored webhook payload.

  3. 3

    Mapping

    One sandboxed expression per field. lookup() translates the ERP's warehouse code through a CSV table in your repository.

  4. 4

    Hooks

    When expressions are not enough, a PHP class from your project steps in.

  5. 5

    Target

    A connector and its operation. stock.update writes Magento's stock. Others are price.update, product.upsert and, with an add-on connector, fakturoid: invoice.create.

  6. 6

    Rules

    What to do with a SKU the shop does not have, and a safety limit: if more than 30% of the records change at once, the run pauses for approval.

How it works

From a file to a run you can trust

There is no setup screen to click through. Everything an install does is in your repository, and CI deploys it.

  1. Describe the flow

    Write the YAML: source, mapping, target. Every flow, record type and connector is checked against its schema and the shop's attributes.

    deedspan:flow:validate
  2. Dry run on a sample

    Map a sample input and see what would be written, record by record. A dry run never touches a target or the database, so CI runs it on every change.

    deedspan:flow:dry-run
  3. Deploy and run

    CI deploys to staging, then production. Workers take each run from RabbitMQ and process it in chunks, so large files run in bounded memory.

    deedspan:flow:run
  4. Know what happened

    Every record's result is stored: written, unchanged, skipped, failed or deleted. Failures are retried with back-off, then put on the failed list for replay.

    results per record, logs in JSON

Send the same file twice. Nothing happens twice.

Every record carries a key, and records are compared with what the flow last wrote or with what the target holds now. A re-sent file, a retried chunk or a replayed run never duplicates an order, an invoice or a stock change.

Observability

See every record, every run

An integration that fails quietly is worse than none. Deedspan stores what happened to each record, tells you when a flow fails or goes silent, and lets you send a record again.

An example run of the flow stock-from-erp: three chunks done, 212 records written, 2,196 unchanged, 3 skipped and 1 failed. The failed record STOVE-9 at location east names the field and the error, and can be replayed.

stock-from-erp

Run 01a1d668 · schedule · 14:15:02 · 41 s

Completed, 1 failed
Written
212
Unchanged
2,196
Skipped
3
Failed
1
  • TENT-200|mainquantity 15written
  • BOOT-42|eastquantity 3unchanged
  • LAMP-7|mainno such locationskipped
  • STOVE-9|eastquantity 8failed

STOVE-9|east · field sku · chunk 3

The shop has no product with this SKU.

Replay record Show payload

stderr, one JSON line per event

{
  "level_name": "WARNING",
  "message": "Record failed.",
  "context": { "event": "record.failed" },
  "extra": {
    "run_id": "01a1d668-1076-746d-9a19",
    "flow": "stock-from-erp",
    "stage": "send",
    "chunk": 3,
    "record_key": "STOVE-9|east"
  }
}

Every log line carries the run, flow, stage, chunk and record key, so you can follow one record across workers.

A result for every record

Written, unchanged, skipped, failed or deleted, with the field and the error. Run history and per-record results are kept after the payloads are gone.

Payloads kept for 30 days

What came in, what was mapped, what was sent and what the target answered. Enough to see why a record failed and to replay it.

Alerts, failed list, replay

A failed run or a paused safety limit raises an alert by e-mail or webhook. Failed records wait on a list per flow until you replay or resolve them.

OpenTelemetry, any backend

Traces and metrics over OTLP: one trace per run, with records read, written and failed, run duration and queue lag. Off unless you configure it, and it works with any OTLP backend.

A heartbeat per flow

A flow calls its heartbeat URL after each successful run. When a flow silently stops running, your monitor notices and raises the alarm.

Errors where you track them

Optional error reporting through the Sentry SDK. A failed order export is also flagged on the order in Magento admin, with the target's error and a retry button.

On installs Deedhold runs, we see it before you do

Logs, errors, heartbeats, uptime and on-call go to one system, Better Stack. When a flow fails or goes silent at night, Deedhold's on-call gets the alert, not your customer service the next morning.

Running it

Built for the day something breaks

An integration is judged on its bad days. Deedspan tells you what failed, why, and lets you send it again.

Retries, failed list, replay

Failures are retried with growing delays. What still fails lands on a failed list per flow. Replay one record or a whole run.

Alerts

Failed runs, paused safety limits and flows that have gone silent raise an alert by e-mail or an event webhook.

Staging and production

Two installs of the same project. CI deploys a change to staging first, and production gets the same image after an explicit promotion.

Command line and API

Flows, runs, per-record results, the failed list, replay, approvals and health. Use them from a terminal, from CI or from your own tools.

Inside Magento admin

An Integration column in the order grid shows exported, pending or failed, with the external number. A failed order shows the target's error and a retry button.

Safety limits

A half-empty export should not empty your shop. A run that would change or delete too much pauses and waits for approval.

Licence

Source-available, yours to keep.

An integration platform holds the logic your business runs on. You should be able to read it, run it and keep it, whoever you work with.

You get the source

Every Deedhold client receives the source of the core and of the add-ons they use. Read every line your business relies on.

Free to use and self-host

Run the core for your own shop on your own servers, change it, and build or sell connectors for it. There is no licence fee for the core.

Leave, and keep everything

If you stop working with Deedhold, your install keeps running. Your flows, mappings and hooks were in your own git repository all along.

MIT after two years

Every release becomes MIT-licensed two years after it is published. That is fixed in the licence, FSL-1.1-MIT, and cannot be taken back.

The one limit: for two years after a release, you may not offer it to others as a competing hosted service. Using it, self-hosting it and implementing it for clients who run it themselves are all allowed.

Getting started

A first flow in five commands

An install is a small project of your own: the application, MariaDB and RabbitMQ in one Compose file. The skeleton comes with an example flow, a lookup table and a sample file.

  • Validate the project: record types, connectors, flows.
  • Dry run the example flow on the sample file.
  • Run it, then run it again: every record is unchanged.

terminal

$ composer create-project deedhold/deedspan-skeleton acme-integrations
$ docker compose up -d

$ bin/console deedspan:flow:validate
Record types: category, price, product, stock_level
Connectors: magento
Flows: stock-from-erp

$ bin/console deedspan:flow:dry-run stock-from-erp samples/stock.csv
TENT-200|main: sku=TENT-200, location=main, quantity=15
BOOT-42|east: sku=BOOT-42, location=east, quantity=3
LAMP-7|main: sku=LAMP-7, location=main, quantity=0

$ bin/console deedspan:flow:run stock-from-erp --sync
Run 01a1d5f1 completed: written 3, unchanged 0, skipped 0, failed 0.

Managed by Deedhold

Let Deedhold build and run it for you

Deedhold writes your flows, hosts your staging and production installs and watches them every day. A flat monthly fee per connected system, no volume fees. The code and the integrations stay yours.

Prefer to run it yourself? Start with the docs.