Skip to main content

Current version

v1. All endpoints live under https://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. 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:
  • recipientEmail must be the buyer’s company email or the email of an active contact of the buyer (case-insensitive). Any other address answers 422 naming recipientEmail. 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, CANCELLED or EXPIRED takes no new invitation: 409. A decision also revokes every invitation of that KYC, which the list then reports as REVOKED.
The same rules hold for links created in the Novatrade24 app. Paths, the request and the response shape are unchanged.

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 with type: 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. 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: true makes a link that never expires; its expiresAt is absent. Combined with expiresInDays it answers 422. This replaces the rule of the entry below that invitations this API creates always expire.
  • creditedContactId credits 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 answers 400, including a contact of another seller of your organization. Without it the link’s prospects are credited to the seller company, as before.
  • paymentSetting and documentUploadSetting (ENABLED, DISABLED or SELLER_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 uuid is the one the person had. Before, the response carried a new uuid and the archived contact stayed behind it. A client that stored the old uuid now gets that same uuid back.
  • 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 answers 409 and 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.
Why. One person now holds one contact per company, and re-joining a company brings back that contact rather than adding a second one, on every path into the platform. Recording the person again left two records for one human being at the same company. 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. TransportVinState was removed with TransportResponse.vins in 1.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 a 1.2.0-draft model 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 status can trail its OrderVinResponse.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 409 on 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 DELIVERED while that shipment’s vehicles read NOT_STARTED in vins. Every shipment is created with a transport, so this happens only when staff remove one. If you need every vehicle to have arrived, read vins.
  • The served document now names TransportStatus. Both status fields $ref it, as the contract always did; the running service used to publish an inline copy of the same three values. Nothing on the wire changes.
The compliance rollup’s 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 gained shipmentIds, 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}/payment asked only the lowest-numbered shipment and answered 404 “This order has no payment.” — while GET /orders/{uuid} answered 200 with status: PAYMENT_SUBMITTED for the same order, because the order’s status asks every shipment. Two operations of one API contradicted each other and the 404 asserted something false.
  • POST and PATCH /orders/{uuid}/payment answered 409, as they still do, because the order alone does not say which half is meant.
So there was no operation at all that could see or correct that payment. New: four shipment-scoped operations. /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. A PAYMENT_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 call 1.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}/invoice with type: FINAL. That operation has always written the row, takes the same InvoiceRequest and returns the same InvoiceResponse. Two URLs writing one row meant two idempotency scopes and two If-Match versions 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, ExportFileStatus and ExportFileSupersededVersion schemas go with them, and so does step 7 of the workflow in info.description.
  • ComplianceResponse loses decision, and the ComplianceDecision schema goes. VALID_EXPORT and VAT_APPLICABLE were a verdict nothing computes. → The VAT question is answered by InvoiceResponse.vatIncluded, which is what was declared on the invoice, by the party that issued it — a recorded fact rather than a derived opinion.
  • ComplianceResponse loses version, and its ETag becomes an opaque validator. There is no rollup row and so no rollup version; the number invited if (version > cached) and could not support it. 304 and If-None-Match are unchanged and are still the reason the endpoint exists — compare the validator for equality, do not order it.
  • ComplianceStatus loses INVALID and gains CANCELLED. Not a pure removal, so read this one: INVALID was the same missing verdict as decision, while CANCELLED is a state the platform genuinely has and this enum could not report — a cancelled order used to announce INCOMPLETE forever. → INVALID has no replacement, because no order was ever in it; CANCELLED is new.
  • OrderStatus loses COMPLIANCE_INVALID and OrderVinComplianceStatus loses INVALID — the same verdict again, in the order family. This one changes a live operation: GET /orders?status=COMPLIANCE_INVALID used to answer 200 with an empty page and now answers 422. The empty page was never anything else; no order has ever carried the value. → no replacement; a closed order is COMPLIANCE_VALID and a cancelled one is CANCELLED.
The rollup gets stronger, not smaller. 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 answers 200 instead of 304 and the first PATCH carrying an old tag answers 409. One-time — re-read the order to pick up its new tag.
  • If-Match on PATCH /partners/{sellerId}/orders/{uuid} narrows to the order record. It refuses only when that record itself moved: its purchaseDate, 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 derived status, 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-Match is unchanged in scope. The tag still covers the whole published body, so a conditional read still answers 200 when the derived status moves, a VIN changes, or the contact’s details are corrected. That is how you learn an order progressed.
This partly supersedes the bullet “Every order ETag changes with it, and keeps changing” under 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/revoke is gone, and with it the rule that made it necessary: pickup was never immutable-once-set. Call POST /transport/pickup again to correct a wrongly recorded date. This is the same removal POST /payment/ready-for-pickup got in 1.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 against 1.2.0-draft or earlier, the method for it is dead code; delete it.
  • TransportType is renamed TransportPickupState, and COMPANY_TRANSPORT is renamed TRANSPORT_COMPANY. The old spelling is not a value the platform can store, so no request carrying it has ever been able to succeed.
  • TransportStatus loses IN_PROGRESS and PARTIAL. A transport records two dates and nothing between them: between pickup and delivery the status is PICKUP_CONFIRMED. Neither removed value could ever be emitted.
  • TransportResponse loses vins[] (and the TransportVinState schema) and dailyViesChecksActive. TransportPickupRequest loses driverIdentification and collectionAddress.
  • TransportDeliveryRequest.arrivalConfirmation (a free string) becomes arrivalConfirmed (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 the TRANSPORT_CONFIRMATION_OF_ARRIVAL document.
One field is added. 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 — COMPANY or CONSUMER.
  • 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 answer 404 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}/contacts
  • PATCH /partners/{sellerId}/buyers/{buyerId} (companyName, address)
  • POST /partners/{sellerId}/buyers/{buyerId}/block and .../unblock
Nothing narrowed: the set of buyers a write reaches is now exactly the set the reads already reached, and a share that has been revoked or has expired still reaches none. See Buyer onboarding for what to design for — above all that your 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}/contacts answers 200 for every buyer the seller can resolve. Its 403 for a granted-capability failure is gone; a {buyerId} outside the seller’s scope now answers 404, like every other buyer-scoped operation.
  • POST /partners/{sellerId}/buyers/{buyerId}/contacts, contacts on POST /partners/{sellerId}/buyers, and responsibleContact on POST/PATCH /partners/{sellerId}/orders are accepted from every organization.
  • responsibleContact is 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/profile is served to every organization whose seller can reach the submission. The 404 for 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-Match answers 200 instead of 304, and If-Match on PATCH /partners/{sellerId}/orders/{uuid} answers 409 — 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 its version stands still. Store the tag per order, treat it as opaque, and re-read on 409 rather than deriving If-Match from version.
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.
Your client must ignore unknown fields and must accept unknown enum values gracefully, wire this assumption into your deserialization.

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 schemaVersion in parallel).
Breaking changes ship as v2 on a new URL path (/v2/...).

Deprecation policy

  • Breaking changes are announced at least 12 months before removal.
  • v1 remains available for at least 12 months after v2 launches.
  • Deprecated endpoints return Deprecation: true and Sunset: <date> headers (RFC 8594) on every response during the overlap period.
Example headers on a deprecated endpoint:

Schema version on webhooks

Webhook payloads carry a schemaVersion 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 acceptedSchemaVersions on the webhook endpoint ([1] by default).
  • When we ship schemaVersion: 2 for 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:
  1. Ignore unknown JSON fields. Configure your deserializer to skip fields the schema doesn’t know about. New optional fields appear without notice.
  2. Accept unknown enum values. Treat them as “opaque string” and log
    • continue. Never throw on unknown values for our documented enums.
  3. Observe response headers. Deprecation and Sunset; your monitoring should surface these.
  4. Respect media types. Always application/json for requests/responses except multipart uploads and application/problem+json for errors.

Detecting deprecation in CI

Add to your CI pipeline:

Next

Error reference

Problem-type stability guarantees.

FAQ

Common integration questions.