How does my existing software work with Relay?
Any software, without rewriting it. Relay is for software you can't rebuild around Cadreen: an ERP, a billing system, a script. It asks Relay before it acts. Your rules decide, a person approves where the rules say so, and the receipt records what happened.
What does Relay do, and what can't it do?
Software that asks first can't do the thing without a permit that names exactly what it asked for. The permit lasts 15 minutes and is used once. Where a rule says a person decides, nothing is permitted until they do.
Looking for the OpenAI-compatible endpoint? It's now the Drop-in API: how to swap it in.
Relay is cooperative. It can't stop software that never asks, and it can't tell whether the person who approves is the developer whose key asked. Use it where the software can be changed to call Relay before it acts, or where it already sends events you want governed.
How do I connect software I can't change?
Put the Relay sidecar next to it: a small program that only makes outbound connections to Cadreen and keeps a small journal on the machine it runs on, so a restart never loses or repeats an action. Three steps, and you never copy a key.
- Get a code. An admin or owner opens the Relay page, chooses Connect your software, and names the connection. The code works once and lasts 10 minutes.
- Pair. On the machine, with the program installed, type the line the page shows:It trades the code for the sidecar's keys and writes them where only you can read them. It then offers to install the sidecar as a service, says exactly what that installs, and asks first. Last it waits for Cadreen to see it.
relay-sidecar pair --url https://<your cadreen> ABCD-EFGH2JKM - Choose what to watch. Give it a database login that can only read, add the database to
sources.json, runrelay-sidecar discoverto look without sending anything, and restart it. Within a minute or two its tables appear on the Relay page, for you to tick the ones that record what your software does. Until you do, nothing your software does is seen or governed.
relay-sidecar status says in sentences what the sidecar on a machine is doing. The sidecar is one program for Linux or Windows, or a Docker image, and each release is signed so you can check it. Download it, with its checksums and how to check them. To install it as a service, run relay-sidecar pair: it says what it would install and asks first. On Windows, install it with install-windows-service.ps1 from the Windows zip, run as Administrator, for now: it locks the sidecar's folder so only the system and administrators can read its keys, and relay-sidecar pair doesn't do that on Windows yet (the next release fixes it).
What is the sidecar, and what can it watch?
It lets Cadreen talk to your old software without changing it. The sidecar sits next to the old system and talks to it too: it reads what the system does, asks Cadreen about it, and writes the answer back where the system can see it.
- Cadreen never connects in. The sidecar makes outbound connections to Cadreen. Two of its sources listen on your own network: HL7 (limit it with
allow_from) and the local web address for software that calls it. - It has keys of its own. Pairing gives it two keys, each for one door, and it refuses your workspace's standard key.
- A restart never loses or repeats an action. It keeps a small journal on the machine, and a retry sends the same request under the same key. Keep its data folder when you upgrade.
- If it can't reach Cadreen, it waits. When its buffer is full it stops reading, and the records stay where they are.
What it can watch:
- A database, by looking only. Give it a login that can only read, on Postgres, SQLite, MySQL or SQL Server. It proves the login can't change anything before it reads, and refuses one that can.
- A database table it records or holds. An outbox table tells Cadreen what already happened. A request table holds the action: the old system writes a request row, and the sidecar makes the real write only once it has a permit.
- Files dropped in a folder. CSV, fixed-width with a COBOL copybook, EBCDIC and X12, from a folder on the machine or fetched from an SFTP folder. FTP isn't supported.
- HL7 v2 messages, sent over MLLP the way hospital systems send them.
- A SOAP service, which it polls.
- A local web address, for software that can call one but not Cadreen's way. It listens on the machine itself only, and holds each request until it may go on.
How far each of these has been tried is in What kinds of software work today? below.
What it does with what it watches:
- Watch first, and change nothing. With a database login that can only read,
relay-sidecar watchlists the tables it could follow. For the ones you choose, it adds up what your software does each day and sends Cadreen only the totals: counts, sums, the largest amount, how many were over a limit you set. No row leaves your network, and table and column names leave only if you allow it. - Record what already happened. An outbox table tells Cadreen after your software has acted. Cadreen keeps the record, but it can't stop what already happened.
- Hold an action until it may go ahead. A request table, a dropped file or the local web address holds what your software wants to do. The sidecar asks Cadreen, waits for a person where your rules say so, redeems the permit, and only then lets it go. If it stops while acting, it reports “unknown” and a person is asked whether it happened. It never acts twice.
- Write the answer back. Beside a request row, and beside an outbox row if you set it to, it writes the result: the operation, how sure Cadreen is, and the receipt code with one line about what happened. A dropped file gets a results file beside it, and an X12 file a 997 acknowledgement. HL7, SOAP and the local web address have nowhere to keep a later answer, so nothing is written back for them.
- Say why it stopped. A source stops only for a reason it can say in one sentence, with the one thing that fixes it. It starts again by itself only when the cause is plainly gone: new keys after you pair again, or a source it can read again. Anything else waits for a person to run
relay-sidecar resume. - Report in. Every 30 seconds it tells Cadreen it's alive, with its version, how much is waiting and how each source is doing, so the dashboard shows whether it's working. No record's contents go with it.
- Let the dashboard name a database, if you allow it. An admin can let Cadreen tell one sidecar which databases to watch. It's off until they turn it on. Cadreen sends only the name of a setting on the machine, never an address or a password, and the machine must list that name in
RELAY_SIDECAR_SOURCE_ENVS. The same read-only check runs before anything is read.
On the machine, relay-sidecar status says in sentences what it's doing, verify is a dry run that sends nothing for real, discover looks at a database without sending anything, watch adds a database in one step, and resume starts a stopped source again.
Where it runs. Linux (64-bit Intel and AMD, or 64-bit ARM), Windows (64-bit), or the Docker image for Linux on either. There's no macOS build and no Windows ARM build. It can run as a service on Linux with systemd and on Windows.
Check what you download. Each release comes with SHA256SUMS and a signature for it. Check the signature with cosign, then check your file against the sums with sha256sum -c. The first release, 0.3.0, was built and signed by hand with Cadreen's release key. Later releases are signed by the release build itself. The download page shows the exact check for the version you downloaded. Confirm the release key's fingerprint with Cadreen another way too, such as through support.
How does my software ask?
Ask with the action and the exact arguments:
POST /api/v1/cadreen/relay/ask
curl -X POST https://api.cadreen.com/api/v1/cadreen/relay/ask \
-H "Authorization: Bearer $CADREEN_RELAY_KEY" \
-H "Idempotency-Key: pay-S-4412-2026-10" \
-H "Content-Type: application/json" \
-d '{"action":"pay_supplier","arguments":{"supplier":"S-4412","amount":"4412.00"}}'The answer is allow with a permit (HTTP 200), deny (HTTP 200: the rules refuse it, don't act), or held (HTTP 202): a person decides, and the permit arrives on the same action once they approve. Poll it, or read the feed below:
GET /api/v1/cadreen/relay/actions/{operation_id}
Just before acting, redeem the permit with the same arguments. Cadreen checks them against what was asked:
POST /api/v1/cadreen/relay/permits/redeem
curl -X POST https://api.cadreen.com/api/v1/cadreen/relay/permits/redeem \
-H "Authorization: Bearer $CADREEN_RELAY_KEY" \
-H "Idempotency-Key: redeem-pay-S-4412-2026-10" \
-H "Content-Type: application/json" \
-d '{"permit":"crp1.…","arguments":{"supplier":"S-4412","amount":"4412.00"}}'Then say what happened. A report of unknown opens a question for a person: did this happen?
POST /api/v1/cadreen/relay/actions/{operation_id}/outcome
curl -X POST https://api.cadreen.com/api/v1/cadreen/relay/actions/$OPERATION_ID/outcome \
-H "Authorization: Bearer $CADREEN_RELAY_KEY" \
-H "Idempotency-Key: report-pay-S-4412-2026-10" \
-H "Content-Type: application/json" \
-d '{"status":"done","evidence":{"external_ref":"PAY-77"}}'The receipt says the work was reported by your software, not seen by Cadreen, because Cadreen never watches the act.
What must my software do?
- Send
Authorization: Bearer <key>on every request. An admin makes a Relay key on the Relay page in the dashboard. - Send an
Idempotency-Key(1 to 255 bytes) when you ask, redeem, report or send an event. The same key with the same body always gets the same answer, so a retry after a lost response is safe. A dry run and a question take no key. - Send JSON of at most 64 KB. Numbers that aren't exact in 64 bits, and money, go as strings:
"4412.00", not4412.00. - Act only when the action's
stateispermittedand the permit hasn't run out.answeris what the rules said when you asked (allow,heldordeny);stateis where the action is now, so a held action you may act on readspermittedonce a person approves. Treat every other state as "don't act". - Redeem the permit just before acting, with the same arguments, then report what happened: done, failed, or unknown. Report once.
- Read
error.code, never the message, to decide what to do. A503means send the same request again with the same key.
How does my software hear what happened?
Read the feed. It lists every move of your asks, oldest first, and never skips one. Keep the next_cursor and read again from it; the feed serves 30 days.
GET /api/v1/cadreen/relay/events
curl "https://api.cadreen.com/api/v1/cadreen/relay/events?after=$CURSOR&limit=100" \
-H "Authorization: Bearer $CADREEN_RELAY_KEY"If your software has a public HTTPS address and would rather not poll, an admin can have each move pushed to it, signed and retried. An admin signed in to the dashboard sets this up; an API key can't, a Relay key included. Each delivery carries an X-Cadreen-Webhook-Signature: check it on the raw body, refuse one more than five minutes old, and drop a delivery id you have already handled.
POST /api/v1/cadreen/relay/webhooks
{ "url": "https://erp.example.com/cadreen-events" }How does my software tell Cadreen what happened?
An admin writes down the kinds of event your software may send and approves the list in Decisions. Only those kinds, with only those fields, are accepted. Each kind is either kept as a record, or held for a person to approve before one connected tool runs. An event that runs something waits for a person, unless an admin let that kind run by itself under a rule, up to a daily limit.
Check a sample event before relying on it. A dry run records nothing:
POST /api/v1/cadreen/events/dry-run
curl -X POST https://api.cadreen.com/api/v1/cadreen/events/dry-run \
-H "Authorization: Bearer $CADREEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"source":"billing-app","event_id":"sample-1","type":"invoice.paid","manifest_version":1,"data":{"invoice_id":"INV-1001","amount":"120.50"}}'Then send the real one. The Idempotency-Key must be the source and the event id, billing-app:sample-1, so one change always has one key:
POST /api/v1/cadreen/events
curl -X POST https://api.cadreen.com/api/v1/cadreen/events \
-H "Authorization: Bearer $CADREEN_EVENTS_KEY" \
-H "Idempotency-Key: billing-app:sample-1" \
-H "Content-Type: application/json" \
-d '{"source":"billing-app","event_id":"sample-1","type":"invoice.paid","manifest_version":1,"data":{"invoice_id":"INV-1001","amount":"120.50"}}'Send events with an events key: it reaches the event door and nothing else. Make one in Settings, under API keys, and choose “Send events only”. A Relay key can't send events: it can ask, redeem, report and read the feed, and nothing else.
A sidecar's key is its own. The sidecar's two keys are made when it is paired (relay-sidecar pair), and each reports only as the sidecar it was made for. A heartbeat, report, choice or figures that names another sidecar gets 403 sidecar_mismatch and nothing is read or recorded. A key made by hand in Settings is tied to no sidecar: it gets 403 sidecar_key_unbound on those doors, so pair the sidecar again to get keys that are.
What the sidecars found (GET /relay/discovery, an admin signed in as a person) is paged, because one source can hold hundreds of tables: 10 sources a page by default (limit, 1 to 50), in sidecar and source order. While there are more, the answer carries next_cursor; send it back as after for the next page. A limit or an after that is not as given is 400 invalid_page.
What if my software can only send a webhook?
Square, Shopify and most other software can send a signed web request when something happens, and can't be changed to call Cadreen. Make a source for it: an admin, signed in as a person, gets an address that is shown once. Give that address to the software. Each message is checked against the software's own signature, kept once, and read as one of the events your list of events declares.
Nothing in a message says whose workspace it is: the address does. What doesn't fit your list of events is held and shown, never dropped. Events from a source with no signature, or from a service whose signature can't tell a copy of an old message from a new one, can only be recorded; they never start an action.
No template for your software? Let it send one real event, then show Cadreen what it means instead of writing a mapping: on the Where it comes from tab, choose Show me what it sent, say what kind of event it is, tick the parts that matter and press Teach it. Each part is read as what the example holds (text, a number, true or false), nothing is guessed, and what is taught can only be recorded. Over the API it is POST /api/v1/cadreen/relay/inbound-sources/{id}/teach.
POST /api/v1/cadreen/relay/inbound-sources
curl -X POST https://api.cadreen.com/api/v1/cadreen/relay/inbound-sources -H "Authorization: Bearer $CADREEN_ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"slug":"shop","name":"Corner shop","template":"square"}'The answer holds the address, once, and the steps for the sender. Software like Square only shows its signature key after you give it the address, so you don't need the key yet: paste the address into the software first. Until the key is set the source is “waiting for its key”. It keeps what arrives (at most 100 messages, 10 MiB and 24 hours) and answers the sender 200, and nothing is applied.
PUT /api/v1/cadreen/relay/inbound-sources/{id}/signing-secret
curl -X PUT https://api.cadreen.com/api/v1/cadreen/relay/inbound-sources/$SOURCE_ID/signing-secret -H "Authorization: Bearer $CADREEN_ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"signing_secret":"<the signature key Square gave you>"}'Each waiting message is checked with the key as of when it arrived, and the answer says how many were taken in. If none match, the key is not kept and nothing is thrown away: check the key and paste it again. You can still send signing_secret when you make the source, if you already have it.
To replace a key, send the new one the same way. The old key keeps working for overlap_hours (24 by default, 72 at most, 0 to stop it at once), so nothing is lost while the software switches. DELETE .../signing-secret/previous ends the old key early and POST .../signing-secret/restore brings it back while it still works.
What the software's own request gets back from POST /api/v1/inbound/{address}:
200receivedorduplicate: it is kept, so the software can stop retrying.401verification_failed: the signature did not verify. Nothing was applied, and the answer never says which check failed.404: no source has this address.410source_disabled: the source is turned off.413: a message is at most 1 MiB.429rate_limited: too fast. Nothing was stored; send it again afterRetry-After.503source_full,awaiting_key_fullorunavailable: nothing was stored. Send it again later (Retry-Aftersays when for the first two).
The Relay page's “Where it comes from” tab does the same without code, and shows what didn't fit.
How does my software check a rule before it asks?
Ask the rules a question without recording an ask. The answer is one of a closed set: yes, needs_person or unknown. Handle unknown as "don't act", never as yes.
POST /api/v1/cadreen/decide
curl -X POST https://api.cadreen.com/api/v1/cadreen/decide \
-H "Authorization: Bearer $CADREEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"question":"allowed","action":"pay_supplier","attributes":{"amount":"4412.00"}}'This only tells your software what the rules say today. It permits nothing: to act, ask.
What kinds of software work today?
Checked against the code on 2026-11-04. A row that says "Not yet" is not built, whatever else you may have read.
| Software | Today | What that means |
|---|---|---|
| Software that can make an HTTPS call with an Authorization header | Works today | It asks first, gets a permit for exactly those arguments, redeems it just before acting, and reports what happened. Your rules decide; a person approves where they say so. |
| Software that can send a JSON event | Partly | Events are checked against a list an admin approved. They are kept as a record, or run one connected tool. Fields can be text, numbers, dates, true or false, lists and nested records. An event that runs something waits for a person unless its type lets your rules decide; a rule can also refuse it. |
| Systems with a REST or OpenAPI interface that Cadreen calls | Works today | Connect it as a service. Calls that aren't provably read-only wait for approval. The system needs a public address, JSON bodies, and no sign-in beyond a key, bearer token or basic auth. |
| PostgreSQL databases | Works today | The sidecar can only look: with a login that can only read, nothing in your database changes. Or it watches an outbox table and writes the result back beside each row. |
| MySQL, SQL Server and Oracle databases | Partly | The sidecar can read an outbox table in all three. Looking without changing anything works for MySQL and SQL Server, not yet for Oracle. |
| CSV files from a legacy system | Partly | Tried end to end on Odoo 16 Community's own CSV export: Relay asked about each payment, a person approved or declined, only the approved lines were released, and the result was written back. Name the column that identifies a line (line_id), or export only what is new: an export that repeats old lines asks about them again. The released file is re-written, so its quoting can differ from the original's. |
| Fixed-width, EBCDIC and X12 files | Partly | Built and tested with made-up sample files only. Not yet tried against a real system. |
| Files fetched from an SFTP folder | Partly | Built and tested by the code's own tests only. Not yet run against a real SFTP server. |
| Hospital systems that send HL7 v2 messages (MLLP) | Partly | Built and tested with made-up messages only. Not yet tried against a real hospital system. |
| SOAP and WSDL services | Partly | The sidecar can poll a SOAP service. |
| Message queues and mainframes (IBM MQ, Kafka, CICS) | Not yet | Not connected. If the system can expose a REST interface, it works as one. |
| Email in, desktop programs and terminal screens | Not yet | Not connected. |
What do I do before I point an old system at it?
Tried end to end on Odoo 16 Community's own CSV export, in a test setup. Relay asked about every payment line, a person approved or declined each one, only the approved lines were released, and the result was written back beside the file. The same steps apply to any system that drops a file.
- Export only what is new. Odoo's export repeats every payment each time. If your file repeats old lines, Relay asks about them again, and a line you already approved could be released, and paid, twice.
- Or name the column that identifies a line. Set
line_idin the mapping file (for payments, the payment number). Relay then remembers every line it has decided and never asks about or releases it again, in any file. A line with no identity, or the same identity twice in one file, is refused. The same identity with different details is refused too, and shown in the rejects file. - Write the file, then rename it. Have the old system write under a temporary name and rename it into the folder, or leave it alone for a few seconds before Relay reads it.
- Write the rule and watch the queue. A rule on the action decides which lines wait for a person, and someone has to look at the decisions.
A line is never released twice, in any file, and is not asked about again while one copy of it is waiting for a person. If a second export arrives before the first one's lines are decided, the repeated lines are not asked about again.
Declined a line by mistake? Cadreen won't ask about that line id again. Re-export the payment with a new line id (for example a new payment number in Odoo) and it is asked as a new line.
After a person decides, it can take up to about 3 minutes (seen once in the trial) before the released file appears.
The released file is written again from the lines that were approved. Its quoting can differ from the original: "480.0" becomes 480.0. A program that reads the file strictly should be checked with a sample first.
Only CSV from Odoo 16 Community was tried on a real system. Fixed-width, EBCDIC, X12, SFTP and HL7 were tried with made-up data, or by the code's own tests, and are not claimed as proven.
What does Relay not do yet?
- Cadreen acts inside your network only through a paired sidecar, and only to do an action you set up for it in the sidecar's own settings. You turn this on at the sidecar (
RELAY_SIDECAR_ACT=on) and declare each action there; in this release an action is one write to a request table in PostgreSQL or SQLite. A person approves each request first and says yes to what it will do in plain words. The sidecar then does that one write, once, with the values that were approved. Nothing else starts inside your network. - A recorded event isn't read by agents yet. It is kept, and a remembered one can be searched.