Current version
v1. All endpoints live underhttps://api.novatrade24.com/v1/.... The
spec document is openapi-integration-v1.yaml, currently at
info.version: 1.5.0-draft.
The webhook document, asyncapi-integration-v1.yaml, does not version
separately. It carries the same info.version as the REST document, and its
changes are listed in the changelog below with everything else.
Changelog
The draft is still moving.info.version in the spec document changes with
it, so you can tell two copies apart; the URL path does not, because v1 has
not shipped yet and the v2 rule below applies from the moment it does.
1.5.0-draft — a KYC invitation’s link is returned once (NOV24-2689)
publicLinkUrl of a kind: KYC invitation is only in the response that
created it. The platform now stores a hash of the link’s token, not the
token, so it cannot return the link again. The list
(GET .../kyc/self-service-invitations), the revoke response and a create that
finds the recipient’s live invitation leave publicLinkUrl out, and the
contract no longer marks it required. An Idempotency-Key replay of the create
answers the stored response without the link, and reusing the key for a
different request still answers 422. To send a link again,
revoke the invitation and create a new one. Links created before this release
keep working.
kind: PROSPECT invitations are unchanged and carry publicLinkUrl on every
response.
1.5.0-draft — who receives a self-service invitation, and when (NOV24-2685)
POST /partners/{sellerId}/buyers/{buyerId}/kyc/self-service-invitations
refuses two requests it used to accept. The invitation link opens the
buyer’s KYC form without an account, so:
recipientEmailmust be the buyer’s company email or the email of an active contact of the buyer (case-insensitive). Any other address answers422namingrecipientEmail. Record the person as a contact of the buyer first if the link should go to them.- A buyer KYC whose latest status is
VERIFIED,REJECTED,CANCELLEDorEXPIREDtakes no new invitation:409. A decision also revokes every invitation of that KYC, which the list then reports asREVOKED.
1.5.0-draft — a prospect invitation whose credited member left reads MEMBER_LEFT
SelfServiceInvitationStatus gains MEMBER_LEFT. A kind: PROSPECT
invitation that credits a member (creditedContact) who has since left the
seller used to read ACTIVE, although a buyer opening it is refused. It now
reads MEMBER_LEFT wherever the invitation is returned. Expiry and revocation outrank
it: such a link that also lapsed or was revoked reads EXPIRED or REVOKED.
KYC invitations never carry it. A client that switches over status needs a
case for the new value.
1.5.0-draft — the KYC profile holds the buyer’s documents only (NOV24-2498)
GET /partners/{sellerId}/buyers/{buyerId}/kyc/profile no longer carries a
seller’s documents. The PDF used to add the company documents of the seller
the buyer’s KYC was made for (its own Chamber of Commerce extract, its bank
statement) and the ID images of that seller’s staff to the buyer’s due
diligence. A seller reaching the KYC through shared access received them too.
The profile now holds the buyer’s company documents and the identity documents
of the buyer’s contacts, of a KYC document type, and nothing of any seller.
Paths, status codes and the response shape are unchanged; a profile can only
get shorter.
1.5.0-draft — VAT security deposit wording (NOV24-2489)
The platform, fresh PDF exports and newly recorded document activities now say VAT security deposit invoice and VAT security deposit agreement. Every invoice withtype: PRELIMINARY means the VAT security deposit invoice.
This deposit secures the VAT and is not an advance payment towards the vehicle price.
Keep sending PRELIMINARY, SHIPMENT_PRELIMINARY_INVOICE and
SHIPMENT_DEPOSIT_AGREEMENT where those values are supported. The invoice upload
operation still accepts only SHIPMENT_PRELIMINARY_INVOICE and SHIPMENT_INVOICE;
it does not add an agreement upload. Paths, fields, schemas, webhook values and
billing behavior are unchanged. Uploaded originals, existing exports and stored
activity text are preserved. See Invoice & Payment.
1.5.0-draft — rate limiting is planned, not enforced
429 is no longer declared. GET /me and POST /buyers listed a
429 Too Many Requests response, and the 200 of GET /me listed
X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers.
Nothing sends them: no endpoint limits requests yet. The responses, the
headers and the TooManyRequests, RetryAfter and XRateLimit* components
are gone from the spec document.
capabilities.rateLimitTier on GET /me stays, as information only. When
limits are enforced, the statuses and headers come back with an entry here;
until then see Rate limits.
1.5.0-draft — a contact person can be asked for as Admin or Member
Shape:ContactRequest gains an optional role, OWNER (Admin) or
CONTACT (Member): the person’s account permission on the platform. It is
accepted on POST /buyers/{buyerId}/contacts and in the contacts array of
POST /buyers. Any other value answers 422 with the field named.
The rules are the platform’s. OWNER is granted while the buyer has no Admin
and the seller holds a KYC request of its own on the buyer, which is when a
contact without role becomes the Admin today. Otherwise the person is
recorded or resolved as a Member, or left as they are, and notApplied names
role (contacts[1].role in an upsert). CONTACT keeps a person a Member
where they would otherwise have become the Admin. A current Admin is never made
a Member through this API. A body without role behaves as before.
notApplied on ContactResponse and BuyerResponse can therefore appear on
a 201 for a newly recorded person, when that person did not get the role
asked for.
1.5.0-draft — Mode A is refused for every organization, and parallel buyer creates resolve
kycMode: MARKETPLACE_LED answers 403 for every organization. It used
to be refused only while the organization’s allowMarketplaceLedKyc
capability was false. Nothing records a buyer’s KYC mode yet, so an
organization holding the capability would have had its Mode A buyer created
and published as NT24_LED. GET /me reports allowMarketplaceLedKyc: false
for every organization until Mode A is available; create buyers with
NT24_LED. No response shape changes.
Two POST /buyers for one VAT number under two Idempotency-Keys no longer
create two buyers. The replay store pairs up a retry only under the same
key. Sent at the same time under two keys, both requests used to create a
buyer, and the second of the two was unreachable by vatNumber afterwards. The
second request now waits for the first and answers 200 with the buyer the
first created, as a later request would.
Adding a contact person no longer reopens a KYC review. A contact recorded
through this API while the buyer’s KYC contact check was in review used to
send that check back to the start. The review now continues; the new person is
picked up by the next KYC run.
1.5.0-draft — a document reports the type it was filed with
Vocabulary:DocumentResponse.type is a StoredDocumentType. It was a
DocumentType. StoredDocumentType has every value of DocumentType, plus
UNCATEGORIZED and the eight *_ADDITIONAL_INFORMATION types
(COC_, BANK_, VAT_, CONTACT_, TP_, ADDRESS_, ORDER_ and
PAYMENT_ADDITIONAL_INFORMATION). Every operation that answers with a
DocumentResponse uses it: the six upload operations and
PATCH /documents/{id}. A client that switches over type needs a case for
each new value, or a default.
Uploads do not change. Upload request bodies stay DocumentType, and a
withheld type still answers 422. ComplianceBlocker.documentType stays
DocumentType too, because it names a type for you to upload.
Why. A document filed through the Novatrade24 web app can carry one of
those types, and PATCH /documents/{id} archives or restores any document the
seller owns. It already answered with the stored type, a value the contract did
not declare, so a client generated from the contract could fail to read the
response. The contract now declares what is on file, and the Integration API
keeps working on the same documents as the platform.
1.5.0-draft — prospect invitations are Click2Comply links, and prospects say where they came from
The invitation URL changes. No transition period.publicLinkUrl of a
kind: PROSPECT invitation is now {frontendBaseUrl}/click2comply?ref={code}
instead of {frontendBaseUrl}/prospect/submit/{token}. It opens the
Click2Comply form for the seller, the same form the seller’s own links open.
This applies to POST /partners/{sellerId}/prospect-invitations, to the list
and to the revoke response, including invitations created before this
release. A client that parsed the token out of the old path must read the
ref query parameter instead, or better, treat the URL as opaque.
Shape: ProspectInvitationRequest gains four optional properties.
neverExpires: truemakes a link that never expires; itsexpiresAtis absent. Combined withexpiresInDaysit answers422. This replaces the rule of the entry below that invitations this API creates always expire.creditedContactIdcredits the link’s prospects to one contact of the seller, who must be a live member of that seller with a platform account. Any other id answers400, including a contact of another seller of your organization. Without it the link’s prospects are credited to the seller company, as before.paymentSettinganddocumentUploadSetting(ENABLED,DISABLEDorSELLER_DEFAULT, the default) override the seller’s Click2Comply payment and document upload settings for buyers through the link.
SelfServiceInvitationResponse publishes creditedContact, paymentSetting
and documentUploadSetting on prospect invitations. KYC invitations carry
none of them.
Shape: ProspectResponse gains source, link and creditedContact on
GET /partners/{sellerId}/prospects and GET /partners/{sellerId}/prospects/{uuid}.
source is the form the prospect was submitted through (CLICK2COMPLY, or
PROSPECT_LINK for the retired prospect link form) and is always present.
link is the personal or campaign link it came through, with its id, kind
and label; a campaign link’s id is the uuid of the prospect invitation.
It is absent for the seller’s company link. creditedContact is the seller’s
contact the prospect is credited to, absent for the company. The seller’s
members can change it on the platform. A client that switches over source
or link.kind needs a case for each value.
The prospect reads were always every prospect. They return prospects from
every entry channel, the company link as much as an invitation. The contract
used to describe them as submissions through a shareable link; that wording
is corrected.
Why. The platform now has one Click2Comply flow with company, personal and
campaign links, and an invitation from this API is one of its campaign links.
The Integration API works like the platform, so it hands out the same URL,
offers the same link settings and shows whom each prospect is credited to.
1.5.0-draft — a payment from an unregistered account carries its reason
Shape:PaymentRequest gains an optional sourceAccountComment, and
PaymentResponse publishes it, absent when no comment is on file. It says why
the buyer paid from a bank account that is not registered with Novatrade24.
POST and PATCH /payment, order-scoped and shipment-scoped, accept it; absent
or blank leaves any comment already on the payment untouched, the rule
sourceIban already follows.
Rule: an order whose payment reports ibanConsistent: false cannot be
completed without that comment. The write itself still answers 201 without
one, because the platform saves the answer before anyone has typed a reason and
only asks for the reason when the order is closed. The Integration API works
the same way. A comment staff enter on the platform satisfies it too, since both
are the same comment. NT24 staff can still close an order without it as an
override, for example when a trade partner can no longer be reached, and the
archival PDF then states that.
Vocabulary: ComplianceBlockerCode gains
PAYMENT_REGISTERED_COMMENT_REQUIRED, in the REST rollup and the webhook
blocker alike. The rollup raises it in the payment module for each shipment
the order close would refuse for that reason: a live shipment whose payment
reports ibanConsistent: false with no comment on file, unless NT24 staff
closed it with their override. It names no documentType. A client that
switches over code needs a case for it.
Every payment ETag changes once with this release, because the tag
covers the whole response and the response now has one more field. A client
holding an older tag gets 409 on its next PATCH and re-reads.
Why. The comment is the compliance record of why the payment came from
elsewhere, and the archival PDF prints it next to the answer. Before this
release an integrator had no field to send it in, and nothing on the server
required it.
1.5.0-draft — the KYC invitation revoke refuses unknown keys
POST /partners/{sellerId}/buyers/{buyerId}/kyc/self-service-invitations/{invitationUuid}/revoke
accepts reason only. A body naming any other key, such as revokedAt, now
answers 422 with one errors[] entry per unknown key, instead of 200 with
the key dropped. The prospect invitation revoke already did this, and both take
the same SelfServiceInvitationRevokeRequest body, so they now answer a body
the same way. A caller that may not see the buyer or the invitation still gets
403 or 404 first.
Why. A dropped key read as an applied one: a 200 for revokedAt looked
as if the revocation had been backdated.
1.5.0-draft — the webhook document carries the API version
Shape, webhooks only:ComplianceBlocker.documentType in
asyncapi-integration-v1.yaml is typed. It was a plain string; it is now a
DocumentType enum with the same values as DocumentType in the REST
document. The values on the wire do not change. A generated webhook handler
gains an enum where it had a string, the same one a REST client already has
for the rollup blocker.
info.version of asyncapi-integration-v1.yaml moves from 1.0.0-draft to
1.5.0-draft. The webhook document had not changed its version since it was
written, including in 1.4.0-draft when two of its events were removed, so two
copies of it could not be told apart. From this release it carries the REST
document’s version, and a webhook change gets its entry here.
Why. The webhook payloads repeat vocabularies from the REST document —
ComplianceStatus, and the module, code, required fields and document type of
ComplianceBlocker — because AsyncAPI cannot reference another document.
These copies are now checked against their originals, so the two documents
cannot report different values for these fields. Other copied vocabularies,
such as ViesResult and ProspectType, are not checked yet.
1.5.0-draft — a prospect invitation may never expire
Shape:expiresAt is optional on SelfServiceInvitationResponse. A
generated client that treated it as required must accept it missing.
Prospect invitations are the seller’s Click2Comply campaign links. A member of
the seller can now make one on the platform without an expiry. Such a link has
no expiresAt in GET /partners/{sellerId}/prospect-invitations and its
status is ACTIVE until it is revoked. Invitations this API creates still
always expire, after expiresInDays or 30 days, and KYC invitations always
carry expiresAt.
The list shows campaign links only. A seller member’s personal Click2Comply
link is not an invitation and never appears there.
POST /partners/{sellerId}/prospect-invitations for a seller that closed on
the platform now answers 409 instead of creating a link that could never be
used.
Why. A campaign link can now be made without an expiry, and the list has to
report them as they are rather than as expired.
1.5.0-draft — the KYC profile holds no orders
Behaviour, not shape. No field, status code or enum changes. Nothing to regenerate.GET /partners/{sellerId}/buyers/{buyerId}/kyc/profile returns the buyer’s
customer due diligence (KYC/KYB) only. Before, the PDF also carried this
seller’s orders with the buyer: an Export Compliance File section with each
order, the order comments, and the documents of those orders, their shipments
and their transports. None of that is in the profile now.
Why. The profile is the KYC file. On the platform, an order’s data goes into
an export downloaded from that order, next to the due diligence, and never into
the KYC file itself.
1.5.0-draft — an archived person named again is reactivated
Behaviour, not shape. No field, status code or enum changes. Nothing to regenerate. This reverses a documented behaviour, so check whether your client relies on it. The Integration API follows the same membership rules as the platform. When you name a contact person who was archived at that buyer, the platform now brings back the contact it already holds instead of recording the person again. This applies wherever this API keys a person on an email address:POST /partners/{sellerId}/buyers/{buyerId}/contacts, a contacts
entry on POST /partners/{sellerId}/buyers, and responsibleContact on
POST /partners/{sellerId}/orders and PATCH .../orders/{uuid}.
- The
uuidis the one the person had. Before, the response carried a newuuidand the archived contact stayed behind it. A client that stored the olduuidnow gets that sameuuidback. - The person is live again. The contact returns to the status it had
before it was archived and appears again in
GET .../buyers/{buyerId}/contacts. An identity check they had completed still counts, unless their ID document has expired since. - The owner role follows the rule for a new contact. A reactivated contact keeps the owner role only when the buyer has no owner and you may name one; otherwise it comes back as a contact.
- A removed person with a platform account answers
409. A person with a platform account whom the company removed can only return through the company’s re-invite. Naming them answers409and nothing is written. - A pending removal is left alone. A person whose removal is pending is used as they are, and the pending request is left for the company’s owners.
- A live contact still wins. When the buyer has a live contact under the same address, that one is used, as before.
- Sending back an order’s own archived contact is unchanged. A
PATCH .../orders/{uuid}that echoes the order’s current responsible contact after that person was archived keeps the order as it is and reactivates nobody.
1.5.0-draft — a lapsed verification link refuses closing
Behaviour, not shape. No field, status code or enum changes. Nothing to regenerate.POST /partners/{sellerId}/buyers/{buyerId}/kyc/verify now marks the
verification subject as waiting for identification when it issues the link,
the same way a link sent from the Novatrade24 platform does. If the person
lets that link lapse unused, they count as not identified, and closing an
order that names them is refused until they complete a new identification.
No acknowledgement overrides that refusal, and neither does a new link:
calling POST .../kyc/verify again issues a fresh link, but the order stays
refused until the person has completed the identification. Before, a lapsed
link issued through this API left the person as they were and changed
nothing about closing.
A subject that already has a verdict keeps its status: a verified subject is
still 409, and a person under review, denied or flagged is not moved.
1.5.0-draft — a split order reports each vehicle’s transport
Additive. Nothing is removed, renamed or changes meaning. Regenerate to get the new field; existing code keeps working.GET /partners/{sellerId}/orders/{uuid}/transport answers for the whole
order, and when NT24 staff split an order across several shipments it
reaches a milestone only once every one of its transports does. That is
still true. A
split order with one half delivered reads PICKUP_CONFIRMED, which never
claims a car arrived that did not, but it also gave you no way to see which
vehicles had arrived.
New: TransportResponse.vins. A required array with one
TransportVinState per vehicle of the order, sorted by VIN. Each entry names
the vehicle’s shipmentId (one of OrderResponse.shipmentIds) and carries
status, pickupConfirmed, pickupDate, deliveryConfirmed,
deliveryDate and arrivalConfirmed, derived by the order-level rule from
the transports of that vehicle’s own shipment. A vehicle whose shipment has
no transport yet reads NOT_STARTED. On an ordinary one-shipment order every
entry repeats the order-level state.
- The schema name comes back.
TransportVinStatewas removed withTransportResponse.vinsin1.3.0-draft, when it could not be filled. The new shape keeps the old three fields (vin,pickupConfirmed,deliveryConfirmed) with the same meaning and adds to them, so a1.2.0-draftmodel of it still finds the three fields it knows. - Two vehicles on one shipment always agree. The platform records the
milestones against a transport, which belongs to a shipment; nothing links
a transport to a single VIN. If staff put more than one transport on a
shipment, its vehicles reach a milestone only when every one of those
transports records it. That is the one case where a vehicle’s
statuscan trail itsOrderVinResponse.complianceStatus, which counts the shipment as delivered once any of its transports is. - The writes are unchanged. Pickup, delivery and transport documents are
still addressed by order and still answer
409on a split order. - The order-level fields count transports, not shipments, as they always
have; the wording in earlier drafts said “shipment” and was loose. A
shipment with no transport contributes nothing to them, so an order can
read
DELIVEREDwhile that shipment’s vehicles readNOT_STARTEDinvins. Every shipment is created with a transport, so this happens only when staff remove one. If you need every vehicle to have arrived, readvins. - The served document now names
TransportStatus. Bothstatusfields$refit, as the contract always did; the running service used to publish an inline copy of the same three values. Nothing on the wire changes.
transport module gains vins too, because it
is the same TransportResponse. The rollup’s ETag is a hash of its body,
so every rollup tag changes once: the first conditional
GET /orders/{uuid}/compliance answers 200 with a fresh tag instead of
304. Keep the new tag and carry on. Order tags are unaffected; the order
body has not changed.
A replayed write can lack the field for one day. POST /transport/pickup
and POST /transport/delivery return a TransportResponse, and an
Idempotency-Key replays the response stored with it for 24 hours. A replay
of a write made before this release returns the body as it was stored,
without vins. Read
GET /transport if you need it.
1.5.0-draft — a split order’s payment is addressable
Additive in fields and operations. Nothing is removed and nothing changes meaning. Regenerate to get the new operations; existing code keeps working. One consequence reaches every client, including those that only ever touch ordinary single-shipment orders: every order ETag changes once. The tag’s representation segment is a hash of the published body and that body has just gainedshipmentIds, so a tag stored before this release no longer matches.
The first conditional GET /partners/{sellerId}/orders/{uuid} or
GET /partners/{sellerId}/orders answers 200 with a fresh tag instead of
304. Nothing is needed beyond that one revalidation: re-read, keep the new
tag, carry on. If-Match is unaffected — it is compared on the tag’s
writable-state segment, which is the order’s uuid and version, and a new
field in the body does not move it, so a PATCH carrying a tag stored before
this release still succeeds for an order nobody edited.
An order normally has one shipment, and /orders/{uuid}/payment addresses its
payment by order alone. NT24 staff do split an order into two shipments, and
when the payment was recorded on the half they created, this API had no way to
reach it:
GET /orders/{uuid}/paymentasked only the lowest-numbered shipment and answered404“This order has no payment.” — whileGET /orders/{uuid}answered200withstatus: PAYMENT_SUBMITTEDfor the same order, because the order’s status asks every shipment. Two operations of one API contradicted each other and the404asserted something false.POSTandPATCH /orders/{uuid}/paymentanswered409, as they still do, because the order alone does not say which half is meant.
/partners/{sellerId}/orders/{uuid}/shipments/{shipmentId}/payment serves
POST, GET and PATCH, and .../payment/documents serves POST. Bodies,
ETag semantics, gates and refusals are the order-scoped resource’s; the only
difference is that you name the shipment. They are served for a
single-shipment order too, so you can use one code path.
New: OrderResponse.shipmentIds. A required array of the order’s live
shipment uuids, oldest first — one entry for an ordinary order. It is how you
reach the operations above. Generated clients gain a field. A client that
ignores it reads exactly what it read before — but its stored order tags no
longer match on a conditional read, once, as above, because the field is part
of the body the tag’s representation segment hashes.
Changed: what GET /orders/{uuid}/payment does with a split order. It now
reports the payment wherever it sits, on whichever half, instead of always
the lowest-numbered shipment’s columns. An order that has a payment never
answers 404 again.
A split order with a payment on exactly one half still answers 200, as it
always did — and now with that half’s payment rather than the lowest-numbered
shipment’s. A split order with a payment on more than one shipment changes
from 200 to 409. Such an order has no single payment, so the operation
refuses instead of picking one: previously one half’s paymentDate, amounts,
sourceIban, status, version and updatedAt were published as though
they were the order’s, with nothing in the body saying a second half existed.
The 409 names /shipments/{shipmentId}/payment and lists the uuids of the
halves that carry a payment, so you can read each one without re-reading the
order. Poll this operation for an order NT24 may split, and handle the 409
by reading those shipments. 409 is new on this operation; it was already
declared under the 4XX response.
Changed: the split-order refusal wording on POST, PATCH and
POST /payment/documents. It said “Record it through the staff application.”
It now names the scoped operation and the order’s shipmentIds. The status
code is unchanged. The invoice resource’s wording is untouched — it has no
shipment-scoped form.
1.5.0-draft — a payment confirmation is proof of payment
Not breaking, and nothing to regenerate — no schema, enum or operation changed. One answer did, on data you have already sent. APAYMENT_CONFIRMATION document now satisfies the payment-document
requirement, alongside PAYMENT_BANK_TRANSFER_SCREENSHOT. Either type,
ACTIVE, filed against the order, counts. The upload has always accepted
PAYMENT_CONFIRMATION and has always answered 201, but the platform’s
payment-document rule read the screenshot alone, so an order whose proof of
payment was filed under that type sat at PAYMENT_SUBMITTED with nothing in
any response saying why.
That order answers differently today than it did yesterday, on the same
stored data. PaymentResponse.readyForPickup is true where it was false,
and the order appears under GET /orders?status=READY_FOR_PICKUP where it
answered PAYMENT_SUBMITTED. Readiness is derived on every read, so no
re-upload and no call is needed. If your client reads PAYMENT_SUBMITTED as
“the platform is still waiting on us”, expect orders to leave that rung without
you having sent anything.
Nothing else moved. PAYMENT_SOURCE_OF_FUNDS_DECLARATION still answers
only the cash rule — it is not proof of payment, and it is still required on
its own once cashAmount reaches 10000. The upload’s accepted types and the
DocumentType enum are unchanged.
1.5.0-draft — a different person on a recorded email address is reported, not refused
One refusal is gone.POST /partners/{sellerId}/buyers/{buyerId}/contacts
answered 409 when the firstName, lastName or phone in your body
disagreed with the person already recorded under that email address, and so did
a contacts entry on POST /partners/{sellerId}/buyers. Both now resolve to
the person on file and answer 201 / 200. Nothing that succeeds today
starts failing, and if your client branches on that 409, the branch is now
dead code — read notApplied instead.
Why it had to go. The comparison was against a row you do not own. Every
partner holding a buyer’s KYC result reads its contact people and may add one,
and Novatrade24 corrects a stored name or telephone number after a marriage, a
typo or a changed number. The moment one did, a client still replaying its
original create body stopped matching and every replay answered 409 for
good — a refusal no retry of the same body could clear, with no PATCH on
this API to clear it any other way. Reporting is the same answer
POST /partners/{sellerId}/buyers already gives for a drifted companyName.
The buyer’s own fields went first, and are recorded here late. A
POST /partners/{sellerId}/buyers body that disagreed with the buyer already
stored stopped answering 409 and began reporting the differing fields in
notApplied in 1.1.0-draft (NOV24-1875, merged 2026-09-03). That change
shipped without a changelog entry of its own; it is named here rather than
written back into the 1.1.0-draft sections below, which record the release as
it was published. This release extends that same answer — same field, same
shape — from a buyer’s own fields to the contact people recorded against it.
Three response schemas gain an optional notApplied array, in one shape,
because the three places this API keys a person on an email address now give
one answer:
Each is absent when there is nothing to report, and absent on every read. The
rest of the response carries the person the call actually resolved to, so you
can see who you got. Nothing is ever applied: a stored contact person is
never overwritten through this API. To correct one, ask Novatrade24 support.
A
contacts array naming two different people on one address no longer
drops one silently. Both arms of POST /buyers used to collapse the array on
the email address, first entry wins, and answer 201 or 200 with nothing
said — while the same two people sent as two contact creates got a 409 on the
second. The entries now resolve in order and a later entry under an address an
earlier one used is reported at its own index. The second person is still not
recorded — the address is the key and one person holds it — but you are told
which entry that was. BuyerResponse.notApplied can therefore appear on a
201, which it could not before; a client that reads it only on the 200 of
a resolve will miss this case.
POST and PATCH /orders compare the whole person, not the address alone.
An order naming one person on an address another person already holds used to
be accepted with its responsible contact silently becoming the stored person.
It still resolves to that person — the responsible contact must be somebody the
buyer actually has — but the substitution is now on the response. Read it: that
person is who Novatrade24 holds accountable for the order and who a
Confirmation-of-Arrival chase reaches.
Every order ETag changes once with this release, for the reason
1.4.0-draft gives below: the tag’s second segment is a hash of the published
body, and that body has gained a field. A tag you stored before the release
names nothing after it, so the first conditional read answers 200 instead of
304 — re-read the order to pick up its new tag. If-Match is unaffected.
It compares the writable-state segment alone, and the order record has not
moved, so a PATCH carrying a tag from before the release is still accepted.
The tag changes once, not per caller: notApplied is excluded from the hash
precisely so that a write reporting drift and a read of the same order still
agree on the tag.
1.4.0-draft — the compliance rollup composes, and there is no Export File
Breaking for generated clients, in five ways. Regenerate. This is the same call1.3.0-draft made about transport, applied to the last two families:
what the platform records is what this API reports.
- The whole Finalization tag is gone — three operations and the tag itself.
Each one is replaced or was never real:
POST /finalization/final-invoice→POST /orders/{uuid}/invoicewithtype: FINAL. That operation has always written the row, takes the sameInvoiceRequestand returns the sameInvoiceResponse. Two URLs writing one row meant two idempotency scopes and twoIf-Matchversions for a single invoice; there is now one of each.GET /finalization/export-file→ nothing, because there is no Export File. No entity, no table, no document type, no generator. The endpoint promised a signed transaction dossier the platform has never produced. If you built a download step around it, remove it: nothing was behind it, so no data is lost. The compliance rollup is where the state of a transaction is read.POST /finalization/regenerate-export→ nothing to queue. Same reason.- The
ExportFileResponse,ExportFileStatusandExportFileSupersededVersionschemas go with them, and so does step 7 of the workflow ininfo.description.
ComplianceResponselosesdecision, and theComplianceDecisionschema goes.VALID_EXPORTandVAT_APPLICABLEwere a verdict nothing computes. → The VAT question is answered byInvoiceResponse.vatIncluded, which is what was declared on the invoice, by the party that issued it — a recorded fact rather than a derived opinion.ComplianceResponselosesversion, and itsETagbecomes an opaque validator. There is no rollup row and so no rollup version; the number invitedif (version > cached)and could not support it.304andIf-None-Matchare unchanged and are still the reason the endpoint exists — compare the validator for equality, do not order it.ComplianceStatuslosesINVALIDand gainsCANCELLED. Not a pure removal, so read this one:INVALIDwas the same missing verdict asdecision, whileCANCELLEDis a state the platform genuinely has and this enum could not report — a cancelled order used to announceINCOMPLETEforever. →INVALIDhas no replacement, because no order was ever in it;CANCELLEDis new.OrderStatuslosesCOMPLIANCE_INVALIDandOrderVinComplianceStatuslosesINVALID— the same verdict again, in the order family. This one changes a live operation:GET /orders?status=COMPLIANCE_INVALIDused to answer200with an empty page and now answers422. The empty page was never anything else; no order has ever carried the value. → no replacement; a closed order isCOMPLIANCE_VALIDand a cancelled one isCANCELLED.
ComplianceModules used to declare
its five payloads as untyped objects, which generate as Map<String, Object>
and gate against nothing. Each is now a $ref to the schema the module’s own
operation already returns:
If you generated a client against
1.3.0-draft, these fields were maps and are
now typed. That is a break, and it is the one worth regenerating for.
ComplianceBlocker is typed too. code was a free string documented as a
stable machine key, which is a promise no contract can keep without
enumerating it. It now $refs ComplianceBlockerCode, an enum of exactly
DOCUMENT, KYC_VALIDATION, CONTACT_VERIFICATION and VIN_CONFIRMATION —
the codes the platform emits — and the blocker carries an optional
documentType naming the document to upload. module loses export_file and
is now the five real modules.
message is now described, and the description is a warning. It is English
prose written for a person, not stable across releases, and must never be
parsed. Branch on code and documentType.
VALID does not mean every artifact is on file. Novatrade24 staff can
force-close an order, so complianceStatus: VALID with a non-empty
blockers[] is possible and is not a contradiction. Read it as “closed, with
these items still outstanding” — and if your integration gates on “no
blockers”, gate on blockers[] rather than on the status.
Two webhook events are removed from asyncapi-integration-v1.yaml:
compliance.decision_made and export_file.generated. They announced the
verdict and the file this release removes. The catalog is eight events, not
ten. compliance.status_changed stays — the status it announces still
exists, and its blockers[] now carries the typed code.
The order’s ETag changes shape, and If-Match on the order narrows. No
schema moves and no generated client has a field for either, but you see both on
the wire — the first once, on deploy, the second from then on.
- Wire format. An order’s tag was
"<32hex>"and is now"<32hex>.<32hex>". It stays opaque: compare it for equality, never parse it or split it on the dot. Every order tag you stored before the deploy names nothing after it, so the first conditional read of an order answers200instead of304and the firstPATCHcarrying an old tag answers409. One-time — re-read the order to pick up its new tag. If-MatchonPATCH /partners/{sellerId}/orders/{uuid}narrows to the order record. It refuses only when that record itself moved: itspurchaseDate, the contact person it names, a staff edit of the order row, a cancellation. It no longer refuses because something the body merely projects moved — the derivedstatus, a VIN, or the responsible contact person’s own name, email or phone. A colleague correcting that person’s phone number in the web app no longer costs you the tag you are holding.If-None-Matchis unchanged in scope. The tag still covers the whole published body, so a conditional read still answers200when the derived status moves, a VIN changes, or the contact’s details are corrected. That is how you learn an order progressed.
1.1.0-draft — removed the two personal-data capabilities
below. Its warning that an order’s tag can move while its version stands still
is still true of the read and is no longer true of the write. Its advice is
unchanged: store the tag per order, treat it as opaque, re-read on 409, and
never derive If-Match from version.
1.3.0-draft — transport describes the two milestones the platform records
Breaking for generated clients, in five ways. Regenerate.POST /transport/pickup/revokeis gone, and with it the rule that made it necessary: pickup was never immutable-once-set. CallPOST /transport/pickupagain to correct a wrongly recorded date. This is the same removalPOST /payment/ready-for-pickupgot in1.2.0-draft— an operation that never had a handler, published against a platform that has neither the business event nor the audit trail it promised. If you generated a client against1.2.0-draftor earlier, the method for it is dead code; delete it.TransportTypeis renamedTransportPickupState, andCOMPANY_TRANSPORTis renamedTRANSPORT_COMPANY. The old spelling is not a value the platform can store, so no request carrying it has ever been able to succeed.TransportStatuslosesIN_PROGRESSandPARTIAL. A transport records two dates and nothing between them: between pickup and delivery the status isPICKUP_CONFIRMED. Neither removed value could ever be emitted.TransportResponselosesvins[](and theTransportVinStateschema) anddailyViesChecksActive.TransportPickupRequestlosesdriverIdentificationandcollectionAddress.TransportDeliveryRequest.arrivalConfirmation(a free string) becomesarrivalConfirmed(a boolean). A request body field renamed and retyped — breaking twice over, and the one change on this list a client that only skims the removals will miss. The evidence is still theTRANSPORT_CONFIRMATION_OF_ARRIVALdocument.
TransportResponse.arrivalConfirmed reports the
confirmed_arrival the platform has always stored and the API never
published. It is the same name and the same boolean as the request field
above, which is the point: what you send on delivery is what you read back.
POST /transport/documents accepts three more types than before.
CONTACT_ID_CARD, CONTACT_PASSPORT and CONTACT_DRIVERS_LICENSE join the
four TRANSPORT_* types. A driver is identified by document rather than by a
field on the pickup body, which is what makes removing driverIdentification
a re-shaping rather than a loss.
Three operations declare statuses they did not before. confirmPickup,
confirmDelivery and uploadTransportDocument each declare 422 for a body
the process has no step for. 409 is new on confirmDelivery and
uploadTransportDocument, and means the order is split across several
shipments so the request does not say which transport to record against.
confirmPickup’s 409 changed meaning — handle this one. It already
declared 409, as “Pickup already confirmed”, under the immutability rule this
release removes. A second POST /transport/pickup now succeeds and corrects
the date, so that 409 can no longer occur; the status is reused for the
split-shipment case above. A client that treats 409 from confirmPickup as
“already set, nothing to do” will silently swallow an ambiguous-transport
error. Re-read it as “say which transport”, not “already done”.
1.2.0-draft — pickup readiness is derived, not signalled
Breaking for generated clients.POST /orders/{uuid}/payment/ready-for-pickup
is gone. Readiness is not a state you set; it is a function of data you have
already sent — a recorded paymentDate plus an ACTIVE
PAYMENT_BANK_TRANSFER_SCREENSHOT, and once paidInCash reaches 10000, an
ACTIVE PAYMENT_SOURCE_OF_FUNDS_DECLARATION as well. The operation never had
a handler and its own description said “Not yet available”, so no integrator
can have working code against it; if you generated a client against
1.1.0-draft, the method is dead code, delete it.
Nothing was lost with it. PaymentResponse.readyForPickup stays, and so
does the READY_FOR_PICKUP rung — they are the derived answer, and they are
what you read instead of calling anything.
1.1.0-draft — buyers who are natural persons
Breaking for generated clients.vatNumber is no longer required on
BuyerResponse. A client generated from an earlier draft that declares the
field non-optional stops deserializing the first buyer the platform holds no VAT
number for. Regenerate, or make vatNumber optional — and expect the key to
be absent, not null.
More buyers are returned than before. A buyer who is a natural person, and a
company buyer that never supplied a VAT number, used to answer 404 on both
buyer reads. They answer 200 now. Nothing narrowed.
BuyerResponse gains two required fields:
type—COMPANYorCONSUMER.companyNumber— the Novatrade24 partner number, e.g.NT-4K7M-9XQ2. Every buyer has one, whatever its type.
companyName carries the legal company name for a COMPANY buyer and the
person’s full name for a CONSUMER buyer. Read type for the kind of party;
the presence of vatNumber does not classify a buyer.
GET /partners/{sellerId}/buyers takes vat or companyNumber, both
optional, exactly one required — neither and both are each a 400. A natural
person is reachable by companyNumber only.
The write side is unchanged and stays company-only.
POST /partners/{sellerId}/buyers still upserts by (sellerId, vatNumber),
which is a COMPANY identity. Natural persons are onboarded through the
prospect / Click2Comply flow, not through this API.
1.1.0-draft — a shared KYC record is shared for writing too
More buyers accept your writes than before. A buyer you reach because another partner shared its KYC result with you used to answer404 on four
operations while answering 200 on every read. It no longer does. The platform
holds one KYC record per buyer, and every partner holding it may now:
POST /partners/{sellerId}/buyers/{buyerId}/contactsPATCH /partners/{sellerId}/buyers/{buyerId}(companyName,address)POST /partners/{sellerId}/buyers/{buyerId}/blockand.../unblock
PATCH lands for every partner holding the
record, that the record’s version can move without you, and that a replayed
POST /partners/{sellerId}/buyers can start disagreeing with the row once
another partner has corrected it. That disagreement answered 409 when this
entry was written; it now resolves and reports the differing fields in
notApplied — see 1.5.0-draft above.
1.1.0-draft — removed the two personal-data capabilities
Breaking for generated clients.Capabilities.allowContactPersonalData
and Capabilities.allowKycDocumentDownload are gone from GET /me. A client
generated from an earlier draft that declares either field non-optional stops
deserializing GET /me the moment this ships. Regenerate, or make the two
fields optional and ignore them.
More personal data reaches you than before. Both flags default to off, so
this widens what most organizations receive. The two claimed to decide what an
integrator may see, which is not how the platform models the question: a staff
user with a seller’s KYC access to a buyer sees that buyer’s contact people and
downloads its KYC documents, and an integrator acting for that seller now
inherits exactly the same. {sellerId} scoping and the seller’s own access are
the whole control. Read the list below against your own DPIA, retention
schedule and erasure cascade before you take the release.
Consequences you can observe:
GET /partners/{sellerId}/buyers/{buyerId}/contactsanswers200for every buyer the seller can resolve. Its403for a granted-capability failure is gone; a{buyerId}outside the seller’s scope now answers404, like every other buyer-scoped operation.POST /partners/{sellerId}/buyers/{buyerId}/contacts,contactsonPOST /partners/{sellerId}/buyers, andresponsibleContactonPOST/PATCH /partners/{sellerId}/ordersare accepted from every organization.responsibleContactis published on every order that has one recorded, whatever your organization holds. It used to be omitted for organizations without the flag. It stays optional in the schema, because a handful of orders predating the primary-contact requirement have none — keep the field nullable when you regenerate, or the first legacy order you read fails to deserialize.GET /partners/{sellerId}/buyers/{buyerId}/kyc/profileis served to every organization whose seller can reach the submission. The404for a submission that has never been verified is unchanged.- Every order ETag changes with it, and keeps changing. The tag is a hash
of the published body, and that body has just gained
responsibleContact, so a tag you stored before the release names nothing after it.If-None-Matchanswers200instead of304, andIf-MatchonPATCH /partners/{sellerId}/orders/{uuid}answers409— re-read the order to pick up its new tag. Orders you already received the contact for are unaffected by the release itself. What is new for everyone is that the contact’s name, email and phone are now part of the tag: staff edit those in the web app without touching the order, so from here on an order’s tag can move while itsversionstands still. Store the tag per order, treat it as opaque, and re-read on409rather than derivingIf-Matchfromversion.
dpaVersion and dpaAcceptedAt are untouched: they record which agreement is
in force, which is a different question from what an organization may see.
allowMarketplaceLedKyc and allowMultiVinOrders stay — they describe what
you are contracted to do.
Version in URL path
We use URL-path versioning (/v1/) rather than headers. Reasons:
- Cacheable, bookmarkable, copy-pasteable.
- Obvious in logs and error reports.
- Trivially routable at edge.
- Matches Stripe/GitHub/Twilio convention.
Non-breaking vs breaking
Non-breaking (ship in current version, any time)
- New endpoints.
- New optional request fields.
- New response fields.
- New enum values on fields where the schema documents “may add values”.
- New error problem types.
- New webhook event types.
- Increased rate limits.
Breaking (requires new major version)
- Removing endpoints or fields.
- Renaming fields.
- Changing response types.
- Removing enum values.
- Tightening constraints (stricter validation on existing fields).
- Changing auth flow.
- Changing the signature algorithm on webhooks (bumps webhook
schemaVersionin parallel).
v2 on a new URL path (/v2/...).
Deprecation policy
- Breaking changes are announced at least 12 months before removal.
v1remains available for at least 12 months afterv2launches.- Deprecated endpoints return
Deprecation: trueandSunset: <date>headers (RFC 8594) on every response during the overlap period.
Schema version on webhooks
Webhook payloads carry aschemaVersion integer separate from the URL
version. Rationale: we may need to evolve the data payload for a single
event type without a full major bump.
- Register the schema versions you accept via
acceptedSchemaVersionson the webhook endpoint ([1]by default). - When we ship
schemaVersion: 2for an event, we dual-emit for a 6-month deprecation window. - Your endpoint receives the highest supported version.
Client compatibility requirements
Any API client you build or consume must:- Ignore unknown JSON fields. Configure your deserializer to skip fields the schema doesn’t know about. New optional fields appear without notice.
- Accept unknown enum values. Treat them as “opaque string” and log
- continue. Never throw on unknown values for our documented enums.
- Observe response headers.
DeprecationandSunset; your monitoring should surface these. - Respect media types. Always
application/jsonfor requests/responses except multipart uploads andapplication/problem+jsonfor errors.
Detecting deprecation in CI
Add to your CI pipeline:Next
Error reference
Problem-type stability guarantees.
FAQ
Common integration questions.