Example use case, not a customer story
How an in-house developer team would connect QWTN to its own ERP
A waste company with its own ERP and a small developer team. They want jobs in, signed notes and payments back, and ESG figures in their dashboard.
- 2developers
- 1ERP to connect
- 120requests a minute, per key
- £69Fleet, a month
An invented business. The team, fleet and volume figures are assumptions for this example, not results from a customer.
Who they are and what goes wrong
A waste company with an in-house ERP that holds customers, jobs and the general ledger. A small developer team wants the ERP to stay the system of record for money and customers, and QWTN to handle the transfer notes, signatures and driver side.
Every job is typed twice, once in the ERP and once on a note. Signed notes and payments are chased by phone and keyed back in.
The sustainability team asks for recycling and carbon figures each quarter, and someone exports and merges files to produce them.
- Business
- Waste collection with an in-house ERP
- Team
- Two developers and an operations lead
- Goal
- No re-keying between the ERP and QWTN
- Stack
- Node and a data warehouse, as an example
- Starting point
- CSV files moved by hand every night
How the integration runs
- Job bookedDraft the note from the ERPThe ERP creates a draft note with the waste description, EWC code, client and weight. The job id is the idempotency key, so a retry after a timeout never makes a second note.
- Before collectionAsk for a signatureThe ERP asks the producer to sign by emailed link. This call uses the app API with a full-access key.
- SignedThe ERP is toldQWTN sends a signed note.signed event and the ERP marks the job documented. A collection.completed event follows when the driver finishes.
- PaidPayments come backQWTN raises invoices from billing runs. When one is paid, by payment link or by hand, an invoice.paid event lets the ERP post to the ledger.
- Each quarterPull the ESG figuresThe dashboard asks for recycling rate, landfill diversion and estimated carbon for a period, for all customers or one.
What they use
Typed REST endpoints
Notes, clients, client sites, receiving sites, collections, containers and weighbridge tickets, with read and write scopes. Bulk create takes up to 100 records a call.
EndpointsSigned webhooks
Eight event types, HMAC-signed, retried with growing delays and replayable from the delivery log.
WebhooksIdempotency keys
Send a key on every POST and PATCH. A retry within 24 hours returns the original response.
IdempotencyThe app API
Everything the web app can do is reachable under /api/v1/app with a full-access key, run as the person who made the key.
The app APIAudit events
Read the change history through /api/v1/audit-events with an audit:read key, filtered by record, client, action or source.
Audit trail endpointOpenAPI 3.1
A generated spec to import into Postman, Insomnia or a code generator.
OpenAPI JSON
The calls in code
Real endpoints from the developer docs. Keys, ids and addresses are placeholders.
Create a transfer note from a booked job
A key with the notes:write scope is enough. The idempotency key stops a retry creating a second note.
const res = await fetch('https://qwtn.co.uk/api/v1/notes', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.QWTN_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': 'erp-job-' + job.id,
},
body: JSON.stringify({
waste_description: 'Mixed construction and demolition waste',
ewc_codes: ['17 09 04'],
client_id: job.qwtnClientId,
weight_kg: 1200,
transfer_date: '2026-10-14',
}),
})
const note = await res.json()
// the new draft note: note.id, note.reference, note.status === 'draft'
// Ask the producer to sign. This is an app API call and needs a full-access key.
await fetch('https://qwtn.co.uk/api/v1/app/notes/' + note.id + '/request-signature', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.QWTN_FULL_ACCESS_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
party: 'transferor',
recipientEmail: 'site@client.co.uk',
recipientName: 'Sam Lee',
}),
})Receive and verify a webhook
Check the HMAC of the timestamp and raw body, reject anything older than five minutes, dedupe on the event id and reply fast. Add the endpoint under Settings, Webhooks.
import crypto from 'node:crypto'
import express from 'express'
const app = express()
app.post('/webhooks/qwtn', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('QWTN-Signature') ?? ''
const t = Number(/t=(\d+)/.exec(header)?.[1])
const expected = crypto
.createHmac('sha256', process.env.QWTN_WEBHOOK_SECRET)
.update(t + '.' + req.body)
.digest('hex')
const fresh = Math.abs(Date.now() / 1000 - t) < 300
const valid = [...header.matchAll(/v1=([0-9a-f]{64})/g)].some(([, sig]) =>
crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex')))
if (!fresh || !valid) return res.status(400).end()
const event = JSON.parse(req.body.toString())
if (alreadyProcessed(event.id)) return res.sendStatus(204)
switch (event.type) {
case 'note.signed': markJobDocumented(event.data.object); break
case 'collection.completed': releaseForBilling(event.data.object); break
case 'invoice.paid': postToLedger(event.data.object); break
}
res.sendStatus(204)
})Pull ESG figures for a period
The report runs as the person who made the key, so that person needs permission to view reports. Add customer=CLIENT_ID for one customer, or format=csv, pdf or returns for a file.
const url = new URL('https://qwtn.co.uk/api/v1/app/reports/esg')
url.searchParams.set('from', '2026-07-01')
url.searchParams.set('to', '2026-09-30')
const r = await fetch(url, {
headers: { Authorization: 'Bearer ' + process.env.QWTN_FULL_ACCESS_KEY },
})
const { summary } = await r.json()
// summary.totalTonnes, summary.recyclingRate, summary.landfillDiversion,
// summary.carbonKg (an estimate, with the method in the full report)Copy the change history each night
A key with the audit:read scope can read the audit trail. Add source=api to see only changes made through the API, and page with next_cursor.
curl "https://qwtn.co.uk/api/v1/audit-events?created_after=2026-10-14T00:00:00Z&limit=100" \
-H "Authorization: Bearer $QWTN_AUDIT_KEY"The plan that fits
Fleet, because the API and webhooks start there
REST API access and webhooks are Fleet features. Scoped keys let the team give each job the least it needs: notes:write for the booking call, audit:read for the warehouse copy, and one full-access key kept in a secrets store for the app API.
What would change it. A team that only needs notes, signatures and scheduling, without the API or webhooks, could use Carrier instead. A 30-day trial is available on paid plans.
Compare every plan- Fleet£69 a month, or £690 a year
- Unlimitedpeople on this plan
No VAT charged. QWTN is not VAT-registered, so the price you see is the price you pay.
What to check first
The plan and feature details that matter most for this example.
- The limit is 120 requests a minute per key. Over it you get 429 with a Retry-After header, so page through lists rather than fetching records one by one.
- The app API returns the app’s own JSON. It is stable in practice but not versioned like the typed endpoints, so prefer a typed endpoint where one exists.
- Webhooks are at-least-once and order is not guaranteed. Dedupe on the event id and fetch the record if you need its latest state.
Make your first API call
Create a key under Settings, API access, or start with an account and the demo.
Example use cases are illustrations, not customer stories.