GET /v1/customers
Relaties ophalen.
Queryparameters
limit | Aantal rijen, standaard 50, maximaal 200. |
cursor | Ondoorzichtige cursor uit de vorige pagina. |
updated_since | Alleen wat op of ná dit tijdstip is gewijzigd. |
Voor ontwikkelaars
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. De API en de webhooks zitten in elk abonnement — geen extra module, geen meerprijs.
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.
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.
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.
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.
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.
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.
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 → API-sleutels op het tabblad 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:
| Gebeurtenis | Wanneer |
|---|---|
customer.created / .updated | Een relatie is aangemaakt of gewijzigd. |
contact.created / .updated | Een contactpersoon is aangemaakt of gewijzigd. |
product.created / .updated | Een product is aangemaakt of gewijzigd. |
quote.created | Er is een offerte aangemaakt. |
quote.sent | De offerte is naar de klant verstuurd. |
quote.accepted / .rejected | De klant heeft getekend of geweigerd. |
quote.processed | Een geaccepteerde offerte is omgezet naar facturen en/of abonnementen. |
invoice.created / .sent | Een factuur is opgesteld of verstuurd. |
invoice.payment | Er is een betaling geboekt. Kan ook een deelbetaling zijn — kijk naar amountPaid op de factuur. |
invoice.credited / .written_off | Er is gecrediteerd of afgeboekt. |
invoice.reminder | Er is een herinnering verstuurd. |
subscription.created / .activated | Een abonnement is opgesteld of gestart. |
subscription.invoiced | Er is een termijn gefactureerd. |
subscription.paused / .resumed / .renewed | Onderbroken, hervat of verlengd. |
subscription.cancelled / .ended | Opgezegd 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 rekenen | Wat 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.
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"
}
| Code | Betekenis |
|---|---|
| 400 | De aanvraag klopt niet — een veld ontbreekt, of een waarde valt buiten wat is toegestaan. De melding zegt welk veld. |
| 401 | De sleutel ontbreekt, is onbekend, ingetrokken, verlopen, of wordt gebruikt vanaf een adres dat niet is toegestaan. |
| 403 | De sleutel is geldig maar mist het benodigde recht. |
| 404 | Niet gevonden. Ook het antwoord als de bijbehorende module niet aanstaat bij deze organisatie. |
| 409 | Dezelfde Idempotency-Key is al gebruikt voor een andere aanvraag. |
| 429 | Te 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.
De klanten en prospects van de organisatie.
GET /v1/customers
Relaties ophalen.
Queryparameters
limit | Aantal rijen, standaard 50, maximaal 200. |
cursor | Ondoorzichtige cursor uit de vorige pagina. |
updated_since | Alleen wat op of ná dit tijdstip is gewijzigd. |
GET /v1/customers/:id
Eén relatie ophalen.
POST /v1/customers
Een relatie aanmaken. Het relatienummer wordt automatisch toegekend.
Velden
name | verplicht | Naam van de relatie. |
externalRef | optioneel | Verwijzing naar jouw eigen systeem. |
addressLine, postalCode, city, country | optioneel | Adresgegevens. |
vatNumber | optioneel | Btw-nummer. |
language | optioneel | Documenttaal, bijvoorbeeld nl. |
type | optioneel | customer, prospect, supplier of other. |
status | optioneel | active of inactive. |
PATCH /v1/customers/:id
Een relatie bijwerken. Alleen de velden die je meestuurt veranderen.
Hangen altijd aan een relatie. De eerste contactpersoon wordt automatisch de primaire.
GET /v1/contacts
Contactpersonen ophalen.
Queryparameters
limit | Aantal rijen, standaard 50, maximaal 200. |
cursor | Ondoorzichtige cursor uit de vorige pagina. |
updated_since | Alleen wat op of ná dit tijdstip is gewijzigd. |
customer_id | Beperk tot één relatie. |
GET /v1/contacts/:id
Eén contactpersoon ophalen.
POST /v1/customers/:id/contacts
Een contactpersoon toevoegen aan een relatie.
Velden
firstName / lastName | minstens één | Naam. |
email, phone, mobile | optioneel | Contactgegevens. |
isPrimary, isBilling | optioneel | Primaire contactpersoon en/of factuuradres. |
PATCH /v1/contacts/:id
Een contactpersoon bijwerken.
De catalogus. Inkoopprijs en interne notitie zitten achter het recht products.cost.
GET /v1/products
Producten ophalen.
Queryparameters
limit | Aantal rijen, standaard 50, maximaal 200. |
cursor | Ondoorzichtige cursor uit de vorige pagina. |
updated_since | Alleen wat op of ná dit tijdstip is gewijzigd. |
GET /v1/products/:id
Eén product ophalen.
POST /v1/products
Een product aanmaken. Het productnummer wordt automatisch toegekend.
Velden
name | verplicht | Naam. |
basePrice | verplicht | Verkoopprijs excl. btw. |
unit | verplicht | Eenheid, bijvoorbeeld stuk. |
vatRate | optioneel | Btw-percentage. |
pricePer | optioneel | none voor eenmalig, of een interval voor terugkerend. |
sku, ean, manufacturer, manufacturerNumber | optioneel | Identificatie. |
categoryId, supplierId, labelIds | optioneel | Indeling. |
costPrice, internalNote | recht vereist | Alleen met products.cost. |
PATCH /v1/products/:id
Een product bijwerken.
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).
Queryparameters
limit | Aantal rijen, standaard 50, maximaal 200. |
cursor | Ondoorzichtige cursor uit de vorige pagina. |
updated_since | Alleen wat op of ná dit tijdstip is gewijzigd. |
status | draft, sent, accepted, rejected of superseded. |
GET /v1/quotes/:id
Eén offerte met haar secties en regels.
POST /v1/quotes
Een offerte aanmaken, eventueel meteen met secties en regels. Ontstaat altijd als concept.
Velden
customerId | verplicht | De relatie. |
contactId, reference, date, currency | optioneel | Kopgegevens. |
sections[] | optioneel | Secties 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.
Alleen beschikbaar als de organisatie de module Projecten afneemt.
GET /v1/projects
Projecten ophalen.
Queryparameters
limit | Aantal rijen, standaard 50, maximaal 200. |
cursor | Ondoorzichtige cursor uit de vorige pagina. |
updated_since | Alleen wat op of ná dit tijdstip is gewijzigd. |
GET /v1/projects/:id
Eén project ophalen.
POST /v1/projects
Een project aanmaken.
Velden
name | verplicht | Naam. |
customerId | optioneel | De relatie. |
status | optioneel | open, won, lost, on_hold of done. |
notes | optioneel | Toelichting. |
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.
Queryparameters
limit | Aantal rijen, standaard 50, maximaal 200. |
cursor | Ondoorzichtige cursor uit de vorige pagina. |
updated_since | Alleen wat op of ná dit tijdstip is gewijzigd. |
open | true voor alleen openstaande taken. |
subject_id | Beperk tot één onderwerp. |
GET /v1/tasks/:id
Eén taak ophalen.
POST /v1/tasks
Een taak of interactie vastleggen bij een relatie, offerte, project, factuur of abonnement.
Velden
kind | verplicht | task, call, email, visit, meeting of note. |
subjectType | verplicht | customer, contact, quote, project, invoice, subscription of design. |
subjectId | verplicht | Het id van dat onderwerp. |
title | verplicht | Korte omschrijving. |
body, dueOn, priority, contactId | optioneel | Toelichting, vervaldatum, prioriteit, betrokken contactpersoon. |
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.
Queryparameters
limit | Aantal rijen, standaard 50, maximaal 200. |
cursor | Ondoorzichtige cursor uit de vorige pagina. |
updated_since | Alleen wat op of ná dit tijdstip is gewijzigd. |
status | draft, sent, credited of written_off. |
GET /v1/invoices/:id
Eén factuur met haar regels.
POST /v1/invoices
Een concept-factuur aanmaken, eventueel meteen met regels.
Velden
customerId | verplicht | De relatie. |
reference, issueDate, quoteId | optioneel | Kopgegevens. |
lines[] | optioneel | Zie de regelvelden bij offertes. |
PUT /v1/invoices/:id/lines
Alle regels van een concept-factuur vervangen. Werkt alleen zolang de factuur concept is.
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.
Queryparameters
limit | Aantal rijen, standaard 50, maximaal 200. |
cursor | Ondoorzichtige cursor uit de vorige pagina. |
updated_since | Alleen wat op of ná dit tijdstip is gewijzigd. |
GET /v1/subscriptions/:id
Eén abonnement ophalen.
POST /v1/subscriptions
Een concept-abonnement aanmaken.
Velden
customerId | verplicht | De relatie. |
name | verplicht | Omschrijving. |
interval | verplicht | month, year, quarter en verder. |
startDate | verplicht | Ingangsdatum (YYYY-MM-DD). |
intervalCount, billingMode, reference | optioneel | Cadans en kopgegevens. |
lines[] | optioneel | Zie de regelvelden bij offertes. |
PUT /v1/subscriptions/:id/lines
Alle regels van een concept-abonnement vervangen.