smedr
Home Functies Vergelijken Prijzen Inloggen Demo aanvragen

Voor ontwikkelaars

API-documentatie

Koppel je eigen software aan smedr: relaties, offertes, producten, projecten, taken, facturen en abonnementen ophalen en aanmaken. Eén REST-API over HTTPS, met JSON.

Versie 1.0.0 · basis-URL https://api.smedr.nl/v1 · bijgewerkt 26 augustus 2026 · openapi.json

Inhoud

  1. Authenticatie
  2. Rechten
  3. Paginatie
  4. Alleen ophalen wat gewijzigd is
  5. Schrijven en herhalingen
  6. Grenzen
  7. Webhooks
  8. Foutmeldingen
  9. Relaties
  10. Contactpersonen
  11. Producten
  12. Offertes
  13. Projecten
  14. Taken en interacties
  15. Facturen
  16. Abonnementen

Authenticatie

Elke aanroep gaat over HTTPS en draagt een API-sleutel mee als bearer-token:

curl https://api.smedr.nl/v1/quotes \
  -H "Authorization: Bearer smedr_live_..."

Een beheerder maakt sleutels aan in smedr onder Instellingen → API-sleutels. De sleutel wordt daar één keer getoond en daarna nergens meer bewaard — ook niet door ons.

Een sleutel bepaalt zelf bij welke organisatie hij hoort. Er zit dus geen organisatie- of accountnaam in het pad, en je kunt met een sleutel nooit bij gegevens van een andere organisatie.

Behandel een sleutel als een wachtwoord. Zet hem nooit in front-end-code of in een openbare repository; hij hoort thuis op een server of in een geheimenkluis.

Rechten

Per sleutel wordt aangevinkt wat hij mag, per gebied en gesplitst in lezen en schrijven. Ontbreekt een recht, dan antwoordt de API met 403 en noemt hij welk recht mist.

Schrijven impliceert lezen: een sleutel die relaties mag aanmaken, mag ze ook ophalen.

Inkoopprijs en interne notitie op een product zitten achter een apart recht (products.cost). Zonder dat recht komen die velden niet in het antwoord, en worden ze bij het schrijven geweigerd in plaats van genegeerd.

Paginatie

Lijsten geven data en nextCursor terug. Is nextCursor gevuld, dan is er een volgende pagina; geef hem mee als ?cursor=. Is hij null, dan ben je klaar.

GET /v1/products?limit=100
GET /v1/products?limit=100&cursor=MjAyNi0wOC0yNlQxMDoxMjowMy4xMjM0NTYrMDJ8OGMyYg

De cursor is ondoorzichtig: lees er niets uit en bouw hem niet zelf. limit is standaard 50 en maximaal 200; een hogere waarde wordt afgekapt, niet geweigerd.

Alleen ophalen wat gewijzigd is

Geef ?updated_since= mee met een tijdstip, dan krijg je alleen wat op of ná dat moment is gewijzigd. Bewaar de updatedAt van je laatste synchronisatie en gebruik die de volgende keer.

GET /v1/quotes?updated_since=2026-08-26T10:00:00Z

Gearchiveerde offertes komen mee met archived: true in plaats van te verdwijnen. Zo ziet je koppeling dát er iets veranderd is; zou een offerte stil uit de lijst vallen, dan zou je denken dat er niets gebeurd was.

Schrijven en herhalingen

Elke POST vereist een Idempotency-Key: een waarde die jij per aanvraag zelf verzint. Komt dezelfde aanvraag nog eens binnen — bijvoorbeeld na een time-out waarbij je niet weet of hij is aangekomen — dan krijg je hetzelfde antwoord terug in plaats van een tweede relatie of factuur.

curl -X POST https://api.smedr.nl/v1/customers \
  -H "Authorization: Bearer smedr_live_..." \
  -H "Idempotency-Key: order-2026-0912" \
  -H "Content-Type: application/json" \
  -d '{"name":"Voorbeeld BV","city":"Eindhoven"}'

Hergebruik je dezelfde sleutel voor een andere aanvraag, dan volgt 409. Gebruik dus per aanvraag een nieuwe waarde. Bij PATCH en PUT is de header niet nodig: die zijn van nature herhaalbaar.

Velden die het systeem zelf toekent — id, offertenummer, relatienummer, bedragen — worden genegeerd als je ze meestuurt. Totalen worden altijd berekend uit de regels.

Grenzen

Er geldt een limiet van 600 aanroepen per minuut per sleutel. Daarboven volgt 429 met een Retry-After-header die zegt hoeveel seconden je moet wachten.

Een offerte of factuur mag maximaal 500 regels en 50 secties hebben in één aanvraag.

Een sleutel kan een lijst met toegestane IP-adressen dragen. Staat die ingevuld, dan werkt de sleutel alleen vanaf die adressen.

Webhooks

In plaats van blijven vragen of er iets veranderd is, kun je smedr laten melden dát er iets gebeurd is. Een beheerder stelt in de app onder Instellingen → Webhooks een bestemming in: een https-adres van jou, plus de gebeurtenissen waarop je wilt luisteren.

Wij sturen dan een POST met deze body:

{
  "type": "quote.accepted",
  "occurredAt": "2026-08-27T10:12:03+02:00",
  "data": { "type": "quote", "id": "8c2b..." }
}

De melding draagt alleen identificatie, geen inhoud. Haal de resource daarna zelf op via de API. Dat is met opzet: zo is er geen tweede weergave die uit de pas kan lopen, en gaan er geen gegevens in bulk over een kanaal waarvan wij de bestemming niet kennen.

Antwoord met een status in de 2xx-reeks. Alles daarbuiten — en een omleiding — telt als mislukt.

Waarop je kunt luisteren. Een beheerder vinkt per bestemming aan welke van deze gebeurtenissen hij wil ontvangen:

GebeurtenisWanneer
customer.created / .updatedEen relatie is aangemaakt of gewijzigd.
contact.created / .updatedEen contactpersoon is aangemaakt of gewijzigd.
product.created / .updatedEen product is aangemaakt of gewijzigd.
quote.createdEr is een offerte aangemaakt.
quote.sentDe offerte is naar de klant verstuurd.
quote.accepted / .rejectedDe klant heeft getekend of geweigerd.
quote.processedEen geaccepteerde offerte is omgezet naar facturen en/of abonnementen.
invoice.created / .sentEen factuur is opgesteld of verstuurd.
invoice.paymentEr is een betaling geboekt. Kan ook een deelbetaling zijn — kijk naar amountPaid op de factuur.
invoice.credited / .written_offEr is gecrediteerd of afgeboekt.
invoice.reminderEr is een herinnering verstuurd.
subscription.created / .activatedEen abonnement is opgesteld of gestart.
subscription.invoicedEr is een termijn gefactureerd.
subscription.paused / .resumed / .renewedOnderbroken, hervat of verlengd.
subscription.cancelled / .endedOpgezegd of afgelopen.

Een melding voor iets dat jouw eigen koppeling zojuist heeft aangemaakt komt gewoon bij je terug. Wil je die lus vermijden, dan kun je bijvoorbeeld je eigen schrijfacties kort onthouden en de bijbehorende melding overslaan.

Controleer de handtekening. Elke melding draagt een header:

X-Smedr-Signature: t=1787812864,v1=9a3f…

Bereken HMAC-SHA256 over de tekst <t>.<ruwe body> met het ondertekengeheim dat je bij het instellen één keer te zien kreeg, en vergelijk die met v1. Reken over de ruwe body, niet over opnieuw geserialiseerde JSON — dan verandert de tekst en klopt de handtekening niet meer.

const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
const mine = crypto.createHmac("sha256", secret)
  .update(t + "." + rawBody, "utf8").digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(mine), Buffer.from(v1));

Verwerp een melding waarvan t meer dan vijf minuten oud is. Het tijdstip zit ín de ondertekende tekst, dus zonder die controle kan iemand een onderschepte melding opnieuw afspelen.

Herhalingen en volgorde. Mislukt een levering, dan proberen wij het opnieuw met oplopende tussenpozen — een halve minuut, twee minuten, tien minuten, een uur, zes uur, een dag — en geven daarna op. Dat betekent twee dingen voor jou:

Waar je op moet rekenenWat je doet
Een melding kan meer dan eens aankomen.Verwerk hem idempotent: dedupliceer op data.id in combinatie met type, of controleer de staat vóór je iets doet.
De volgorde ligt niet vast.Ga niet uit van "verstuurd komt vóór geaccepteerd". Haal bij twijfel de resource op; die vertelt de huidige stand.
Blijft je endpoint mislukken, dan zetten wij de bestemming uit.De beheerder ziet dat in de app en zet hem weer aan zodra het endpoint werkt.

Antwoord snel — wij wachten maximaal vijf seconden. Doe het echte werk daarna, niet vóór je antwoordt: een trage verwerking leidt tot een time-out en dus tot herhalingen.

Foutmeldingen

Fouten hebben altijd dezelfde vorm, zodat je er één keer op hoeft te programmeren:

{
  "error": "forbidden",
  "message": "Deze API-sleutel heeft het recht 'quotes.view' niet"
}
CodeBetekenis
400De aanvraag klopt niet — een veld ontbreekt, of een waarde valt buiten wat is toegestaan. De melding zegt welk veld.
401De sleutel ontbreekt, is onbekend, ingetrokken, verlopen, of wordt gebruikt vanaf een adres dat niet is toegestaan.
403De sleutel is geldig maar mist het benodigde recht.
404Niet gevonden. Ook het antwoord als de bijbehorende module niet aanstaat bij deze organisatie.
409Dezelfde Idempotency-Key is al gebruikt voor een andere aanvraag.
429Te veel aanroepen. Wacht het aantal seconden uit Retry-After af.

Een 401 maakt geen onderscheid tussen "deze sleutel bestaat niet" en "deze sleutel mag hier niet vandaan". Dat is met opzet: het antwoord mag niet verklappen of een sleutel bestaat.

Relaties

De klanten en prospects van de organisatie.

GET /v1/customers

Relaties ophalen.

recht: customers.view

Queryparameters

limitAantal rijen, standaard 50, maximaal 200.
cursorOndoorzichtige cursor uit de vorige pagina.
updated_sinceAlleen wat op of ná dit tijdstip is gewijzigd.

GET /v1/customers/:id

Eén relatie ophalen.

recht: customers.view

POST /v1/customers

Een relatie aanmaken. Het relatienummer wordt automatisch toegekend.

recht: customers.edit · vereist Idempotency-Key

Velden

nameverplichtNaam van de relatie.
externalRefoptioneelVerwijzing naar jouw eigen systeem.
addressLine, postalCode, city, countryoptioneelAdresgegevens.
vatNumberoptioneelBtw-nummer.
languageoptioneelDocumenttaal, bijvoorbeeld nl.
typeoptioneelcustomer, prospect, supplier of other.
statusoptioneelactive of inactive.

PATCH /v1/customers/:id

Een relatie bijwerken. Alleen de velden die je meestuurt veranderen.

recht: customers.edit

Contactpersonen

Hangen altijd aan een relatie. De eerste contactpersoon wordt automatisch de primaire.

GET /v1/contacts

Contactpersonen ophalen.

recht: contacts.view

Queryparameters

limitAantal rijen, standaard 50, maximaal 200.
cursorOndoorzichtige cursor uit de vorige pagina.
updated_sinceAlleen wat op of ná dit tijdstip is gewijzigd.
customer_idBeperk tot één relatie.

GET /v1/contacts/:id

Eén contactpersoon ophalen.

recht: contacts.view

POST /v1/customers/:id/contacts

Een contactpersoon toevoegen aan een relatie.

recht: contacts.edit · vereist Idempotency-Key

Velden

firstName / lastNameminstens éénNaam.
email, phone, mobileoptioneelContactgegevens.
isPrimary, isBillingoptioneelPrimaire contactpersoon en/of factuuradres.

PATCH /v1/contacts/:id

Een contactpersoon bijwerken.

recht: contacts.edit

Producten

De catalogus. Inkoopprijs en interne notitie zitten achter het recht products.cost.

GET /v1/products

Producten ophalen.

recht: products.view

Queryparameters

limitAantal rijen, standaard 50, maximaal 200.
cursorOndoorzichtige cursor uit de vorige pagina.
updated_sinceAlleen wat op of ná dit tijdstip is gewijzigd.

GET /v1/products/:id

Eén product ophalen.

recht: products.view

POST /v1/products

Een product aanmaken. Het productnummer wordt automatisch toegekend.

recht: products.edit · vereist Idempotency-Key

Velden

nameverplichtNaam.
basePriceverplichtVerkoopprijs excl. btw.
unitverplichtEenheid, bijvoorbeeld stuk.
vatRateoptioneelBtw-percentage.
pricePeroptioneelnone voor eenmalig, of een interval voor terugkerend.
sku, ean, manufacturer, manufacturerNumberoptioneelIdentificatie.
categoryId, supplierId, labelIdsoptioneelIndeling.
costPrice, internalNoterecht vereistAlleen met products.cost.

PATCH /v1/products/:id

Een product bijwerken.

recht: products.edit

Offertes

Een offerte heeft secties met regels. Bedragen worden altijd berekend uit de regels, inclusief de kortingsregels van de organisatie.

GET /v1/quotes

Offertes ophalen (zonder regels).

recht: quotes.view

Queryparameters

limitAantal rijen, standaard 50, maximaal 200.
cursorOndoorzichtige cursor uit de vorige pagina.
updated_sinceAlleen wat op of ná dit tijdstip is gewijzigd.
statusdraft, sent, accepted, rejected of superseded.

GET /v1/quotes/:id

Eén offerte met haar secties en regels.

recht: quotes.view

POST /v1/quotes

Een offerte aanmaken, eventueel meteen met secties en regels. Ontstaat altijd als concept.

recht: quotes.edit · vereist Idempotency-Key

Velden

customerIdverplichtDe relatie.
contactId, reference, date, currencyoptioneelKopgegevens.
sections[]optioneelSecties met title, optioneel optional, en lines[].
sections[].lines[]description, qty, unitPrice verplicht; productId, unit, discountPct, vatRate, optional optioneel.

Voorbeeld

{
  "customerId": "8c2b...",
  "reference": "Aanvraag 2026-0912",
  "sections": [{
    "title": "Werkzaamheden",
    "lines": [
      { "description": "Advies", "qty": 2, "unitPrice": 100, "vatRate": 21 },
      { "description": "Installatie", "qty": 1, "unitPrice": 250, "discountPct": 10 }
    ]
  }]
}

PATCH /v1/quotes/:id

Kopgegevens bijwerken. De status is niet via de API te wijzigen — versturen is een handeling in smedr zelf.

recht: quotes.edit

Projecten

Alleen beschikbaar als de organisatie de module Projecten afneemt.

GET /v1/projects

Projecten ophalen.

recht: projects.view

Queryparameters

limitAantal rijen, standaard 50, maximaal 200.
cursorOndoorzichtige cursor uit de vorige pagina.
updated_sinceAlleen wat op of ná dit tijdstip is gewijzigd.

GET /v1/projects/:id

Eén project ophalen.

recht: projects.view

POST /v1/projects

Een project aanmaken.

recht: projects.edit · vereist Idempotency-Key

Velden

nameverplichtNaam.
customerIdoptioneelDe relatie.
statusoptioneelopen, won, lost, on_hold of done.
notesoptioneelToelichting.

Taken en interacties

Eén lijst voor beide: kind: "task" is iets dat nog moet gebeuren, de andere soorten zijn iets dat heeft plaatsgevonden. Vereist de module CRM.

GET /v1/tasks

Taken en interacties ophalen.

recht: tasks.view

Queryparameters

limitAantal rijen, standaard 50, maximaal 200.
cursorOndoorzichtige cursor uit de vorige pagina.
updated_sinceAlleen wat op of ná dit tijdstip is gewijzigd.
opentrue voor alleen openstaande taken.
subject_idBeperk tot één onderwerp.

GET /v1/tasks/:id

Eén taak ophalen.

recht: tasks.view

POST /v1/tasks

Een taak of interactie vastleggen bij een relatie, offerte, project, factuur of abonnement.

recht: tasks.edit · vereist Idempotency-Key

Velden

kindverplichttask, call, email, visit, meeting of note.
subjectTypeverplichtcustomer, contact, quote, project, invoice, subscription of design.
subjectIdverplichtHet id van dat onderwerp.
titleverplichtKorte omschrijving.
body, dueOn, priority, contactIdoptioneelToelichting, vervaldatum, prioriteit, betrokken contactpersoon.

Facturen

Via de API aangemaakte facturen zijn altijd concept. Definitief maken en versturen gebeurt in smedr: dat kent een factuurnummer toe uit een doorlopende reeks en stuurt post naar een echte klant. Vereist de module Facturen.

GET /v1/invoices

Facturen ophalen, inclusief het betaalde bedrag.

recht: invoices.view

Queryparameters

limitAantal rijen, standaard 50, maximaal 200.
cursorOndoorzichtige cursor uit de vorige pagina.
updated_sinceAlleen wat op of ná dit tijdstip is gewijzigd.
statusdraft, sent, credited of written_off.

GET /v1/invoices/:id

Eén factuur met haar regels.

recht: invoices.view

POST /v1/invoices

Een concept-factuur aanmaken, eventueel meteen met regels.

recht: invoices.edit · vereist Idempotency-Key

Velden

customerIdverplichtDe relatie.
reference, issueDate, quoteIdoptioneelKopgegevens.
lines[]optioneelZie de regelvelden bij offertes.

PUT /v1/invoices/:id/lines

Alle regels van een concept-factuur vervangen. Werkt alleen zolang de factuur concept is.

recht: invoices.edit

Abonnementen

Ook hier: via de API aangemaakte abonnementen zijn concept. Activeren start een terugkerende verplichting en blijft daarom een handeling in smedr. Vereist de module Abonnementen.

GET /v1/subscriptions

Abonnementen ophalen.

recht: subscriptions.view

Queryparameters

limitAantal rijen, standaard 50, maximaal 200.
cursorOndoorzichtige cursor uit de vorige pagina.
updated_sinceAlleen wat op of ná dit tijdstip is gewijzigd.

GET /v1/subscriptions/:id

Eén abonnement ophalen.

recht: subscriptions.view

POST /v1/subscriptions

Een concept-abonnement aanmaken.

recht: subscriptions.edit · vereist Idempotency-Key

Velden

customerIdverplichtDe relatie.
nameverplichtOmschrijving.
intervalverplichtmonth, year, quarter en verder.
startDateverplichtIngangsdatum (YYYY-MM-DD).
intervalCount, billingMode, referenceoptioneelCadans en kopgegevens.
lines[]optioneelZie de regelvelden bij offertes.

PUT /v1/subscriptions/:id/lines

Alle regels van een concept-abonnement vervangen.

recht: subscriptions.edit

smedr
Functies Vergelijken Prijzen Contact API Privacy Verwerkersovereenkomst
Van offerte tot betaling, voor de vakman · met zorg gesmeed.