Skip to content
Van Oosten Advies B.V.

Last updated: September 19, 2026

CustomerGate 365: web service reference

For developers who build a website on CustomerGate 365. It covers authentication, the shape of the endpoints, every service and procedure, the JSON keys and fixed values, and the security rules behind them. It describes app version 1.0. Names are case-sensitive, and the AL source of the app is the authority.

The services

CustomerGate 365 publishes eight web services. The app publishes them itself on installation and on every update: a missing service is created, and a service that already exists under that name is never changed, even if someone unpublished or edited it by hand. The three iFacto services exist only where Steel 365 is installed.

ServiceKindPurposeCalled by
voaItemspage, read-onlyItems for the catalogsynchronization application
voaVariantspage, read-onlyItem variants (sizes)synchronization application
voaCategoriespage, read-onlyItem categoriessynchronization application
voaProfilespage (iFacto DXSteel)Steel profilessynchronization application
voaGradespage (iFacto DXSteel)Steel gradessynchronization application
voaItemTypespage (iFacto DXSteel)Steel item typessynchronization application
voaRequestscodeunit, unbound actionsAccept a quote request or an account requestwebsite application
voaPortalcodeunit, unbound actionsSign-in and everything a signed-in customer sees or doeswebsite application

Authentication

The website talks to Business Central as two Microsoft Entra applications. Their client IDs go into CustomerGate Setup, and Repair Permissions there registers them in Business Central and assigns their permission sets.

Setup fieldUsed forMay call
Website Client IDQuote and account requests, the customer portalvoaRequests, voaPortal
Synchronization Client IDThe catalog synchronization. Leave blank if the catalog is not synchronized.voaItems, voaVariants, voaCategories, voaProfiles, voaGrades, voaItemTypes

Neither application can use the other's services: the website application has no execute right on the catalog pages, and the synchronization application has none on voaPortal or voaRequests.

Permission sets

SetAssigned toWhat it allows
VOA WEB APIwebsite, all companiesExecute voaRequests. Indirect insert only on the request tables, no read.
VOA WEB PORTALwebsite, all companiesExecute voaPortal and the codeunits it calls. Only indirect table rights.
VOA WEB BASELINEwebsite, all companiesThe floor a web service session needs, instead of D365 BASIC, which would expose customer and sales data.
VOA CG EXECUTEwebsite, tenant setExecute rights on the objects the mail, write and PDF paths reach.
VOA CG MAILwebsite, tenant setWhat the e-mail connectors need to send mail, such as the sign-in code.
VOA CG S365 ARCHIVEwebsite, only with DXSteel Doc ArchiveRead the archive tables, with a security filter on the allowed archive categories.
VOA CG DOCUMENTSwebsite, only without that archive and with Portal Documents from Reports onRead what the standard sales reports read, and run them, so Document can print PDFs.
CDC BASICwebsite, only with Continia Document CaptureContinia hooks the triggers of the tables the website writes to.
VOA WEB SYNCsynchronization, per companyThe three catalog pages and their source tables, plus read on item ledger entries for the remaining inventory.
VOA CG S365 SYNCsynchronization, only with Steel 365The three DXSteel pages and the tables behind them.

Getting a token

Both applications use the OAuth 2.0 client credentials flow:

POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token

grant_type=client_credentials
client_id={client id}
client_secret={secret}
scope=https://api.businesscentral.dynamics.com/.default

Send the access token as Authorization: Bearer {token} on every call.

Endpoints and conventions

All services are standard Business Central OData v4 endpoints under https://api.businesscentral.dynamics.com/v2.0/{tenant}/{environment}/ODataV4.

Page services

The catalog pages are read with GET. The reference website addresses the company by name:

GET .../ODataV4/Company('{company name}')/voaItems?$select=No,Description&$filter=...

A company name with an apostrophe has it doubled (O''Brien), as OData requires. Large results are paged: follow @odata.nextLink until it is absent. The records are in value.

Codeunit services

voaRequests and voaPortal expose each public procedure as an unbound action named {service}_{Procedure}. Call it with POST and pass the parameters by name in a JSON body. The reference website addresses the company with its GUID, the id of the company in the standard companies API:

POST https://api.businesscentral.dynamics.com/v2.0/{tenant}/{environment}/ODataV4/voaPortal_ReadSession?company={company id}
Authorization: Bearer {access token}
Content-Type: application/json
Accept: application/json
Accept-Language: en-US

{"token": "<session token>"}

The return value comes back in the OData envelope under value:

{"@odata.context": "…", "value": "{\"email\":\"buyer@example.com\",\"customerNo\":\"C00010\", …}"}

Conventions

  • Names are case-sensitive and bound by name. voaPortal_readSession does not exist, and a parameter called emailAddress instead of email gets a 400 with no further explanation.
  • Every parameter is a string: all procedure parameters are AL Text.
  • A JSON result comes back as a string inside value, because an unbound action cannot return a nested object. Parse value a second time. A Boolean result is a JSON true or false in value.
  • An empty string is an answer. For most voaPortal procedures, "" means no valid session, or nothing for you here.
  • Optional keys are left out, not sent as null. Treat a missing key as null.
  • Dates are YYYY-MM-DD, timestamps are ISO 8601 in UTC (2026-09-19T14:30:00.000Z). An empty date or timestamp is "".
  • Numbers and Booleans are JSON numbers and JSON Booleans.
  • Accept-Language sets the language of the Business Central session. It affects error messages and the option captions in the catalog, never the fixed values below. The reference website sends nl-NL.

Status codes and retries

StatusMeaning here
200The procedure ran. Check value for the outcome.
400An AL error was raised, for example a refused request (the message is in the body), or the parameter names do not match.
401 / 403Token or permission sets are wrong. The body names the object that is missing.
404The service is not published under that name, or the company is wrong.
409Concurrent calls with the same session token tried to update the same session row.
429Business Central is throttling. Nothing was processed; honour Retry-After.
5xxServer error. For a state-changing call you do not know whether it ran.

Retrying after a 429 is always safe. After a network error, a time-out, a 409 or a 5xx, retry only the reading procedures (ReadSession, MyDetails, MyDeliveryAddresses, MyRequests, MyQuotes, MyOrders, MyShipments, DocumentLines, MyDocuments, Document). Do not retry RequestCode, SignIn, SignOut, LinkRequest or AcceptQuote on your own; for CreateQuoteRequest and CreateAccountRequest, a retry is safe only with the same requestId.

Security model

There are two layers. The Entra application proves that the caller is the website; it never identifies a customer. The portal session token identifies the signed-in person, and Business Central derives the customer from it. The customer number never travels from the website to Business Central: no procedure takes a customer number, an e-mail filter or any other parameter that decides whose data is returned.

Portal users

  • A person may use the portal once inside sales has approved their account request. That creates a portal user: an e-mail address linked to one customer, with a role. The website cannot create portal users.
  • E-mail addresses are compared in lowercase, with surrounding spaces removed.
  • If one address is linked to several customers, the session gets the lowest customer number that is allowed in the portal. The interface has no way to list or switch customers.
  • The portal user must be active and not anonymized, and the customer must exist, must not be privacy blocked and must not be blocked for all. Customers blocked for ship or invoice keep portal access.

Sign-in with a one-time code

  1. The visitor enters their e-mail address; the website calls RequestCode(email, ipHash).
  2. Business Central creates a code, stores only a salted hash of it, and e-mails the code to the address if it belongs to an active portal user.
  3. The visitor enters the code; the website calls SignIn(email, code, ipHash) and gets a session token, or "".
  4. The website keeps the token, for example in an HttpOnly cookie, and passes it as token to every other voaPortal procedure.
  5. SignOut(token) ends the session.

The code is ten characters from the alphabet 0123456789ABCDEFGHJKMNPQRSTVWXYZ (no I, L, O or U) and is shown in the e-mail as two groups of five with a hyphen. It is normalized before comparison: spaces and hyphens removed, uppercased, O read as 0, I and L read as 1. Apply the same rules if you validate the input on the website.

  • A code is valid for Sign-in Code Validity (Min.), 15 minutes by default, and works once. After a successful sign-in, every other open code for that address expires.
  • Wrong guesses do not lock anyone out. They are counted on the newest open code up to Maximum Code Attempts, as a trace for abuse detection.
  • Several codes can be alive at once: a new code does not invalidate earlier ones.
  • RequestCode always answers true, whether or not the address is known, linked, blocked or the mail failed, and always takes at least 1.5 seconds. That way the endpoint does not tell anyone which addresses are customers.
  • Between two codes for the same address there is a minimum wait of Sign-in Code Wait Time (Sec.), 60 seconds by default. A request within that time is answered true and does nothing.
  • The code mail is sent immediately from the RequestCode call, not by the job queue, in the language of the account request the portal user came from, else the customer's language, else the inside sales language.

Session tokens

  • A token is 64 characters, two GUIDs as uppercase hex. Treat it as opaque; Business Central stores only its SHA-256 hash. Leading and trailing spaces in token are ignored.
  • Sliding expiry: a session expires after Session Duration (Min.), 480 by default, without use. Every successful call extends it, written at most once per minute, so a session can end up to a minute earlier than the nominal time.
  • Absolute maximum: no session outlives Maximum Session Age (Hours), 168 by default, after sign-in, however actively it is used.
  • Every call checks that the token exists, is not revoked, has not expired and is within the maximum age, that the portal user still exists and is active, and that the customer is still allowed in the portal. If the portal user or customer check fails, the session is revoked at once.
  • The role is read from the portal user on every call, not from the session, so a change from Buyer to Viewer applies immediately.
  • ReadSession returns expiresAt. Use it for the cookie lifetime.
  • Several calls with the same token at the same moment can make Business Central answer 409 to one of them. The reading procedures are safe to retry once.

The customer lock

Everything voaPortal returns is filtered on the customer of the session, and no parameter can remove that filter:

DataFiltered on
Web requestsCustomer No. of the request
Sales quotes and orders, live and archivedSell-to Customer No.
Posted shipments, invoices, credit memosSell-to Customer No.
Ship-to addressesCustomer No.
DXSteel Doc Archiverelation is this customer, relation type Customer, and an allowed archive category

A document number is a parameter in DocumentLines, Document and AcceptQuote. It says what is asked, never whose it is. Document numbers are trimmed and uppercased; a number longer than 20 characters is treated as not found, not truncated.

Uniform empty answers

A document that does not exist and a document of another customer give exactly the same answer. Otherwise a list of numbers would be enough to probe what exists.

ProcedureNo valid sessionNot found, not yours, not allowed
RequestCodenot applicabletrue (always)
SignInnot applicable"" (wrong, expired or used code, inactive user)
SignOuttruetrue
ReadSession, MyDetails, MyRequests, MyQuotes, MyOrders, MyShipments, MyDocuments""not applicable: a list may be empty
MyDeliveryAddresses"""" for a Viewer
LinkRequestfalsefalse
DocumentLines""""; an existing document of yours without item lines gives {"lines":[]}
Document"""", also when no report is set up, or with the Steel 365 archive
AcceptQuote{"outcome":"refused",…}{"outcome":"refused",…}

Roles

A new portal user is a Viewer unless inside sales chooses Buyer, and an unknown role value is reported as Viewer. A Buyer may do everything a Viewer may, plus LinkRequest, MyDeliveryAddresses and AcceptQuote. For a Viewer those three answer false, "" and refused. A Viewer can still submit a quote request through voaRequests; it is then not linked to the customer, and inside sales handles it like a request from an anonymous visitor. MyDetails returns role, so the form can say so.

IP hash

Several calls take an ipHash. Business Central never receives the visitor's IP address: the website computes a keyed hash. The reference website uses HMAC-SHA256 with a secret key from its own configuration, as 64 lowercase hex characters. Business Central stores the value as received, up to 64 characters, on the sign-in code, the session, the quote acceptance and the requests, uses it only to recognize abuse, and the cleanup job erases it after the retention period. Keep the key secret and stable: without a key, an IPv4 hash can be reversed.

Portal settings

Per company, on CustomerGate Setup:

SettingDefaultAllowedEffect
Sign-in Code Validity (Min.)151 to 1440How long a code works.
Maximum Code Attempts51 to 20How many wrong attempts per code are counted. Not a lockout.
Sign-in Code Wait Time (Sec.)600 to 3600Minimum time between two codes for one address. 0 removes the wait.
Session Duration (Min.)4805 to 20160Sliding expiry.
Maximum Session Age (Hours)1681 to 480Absolute end of a session.
Portal Documents from ReportsonSee MyDocuments and Document.
Refuse RequestsoffMakes CreateQuoteRequest and CreateAccountRequest fail with an error. Does not affect the portal.

Catalog services

The catalog services are read-only list pages, meant for a nightly sync rather than for live page views. Use $select to fetch only the properties you need. The OData property names are the names of the page controls listed below and are part of the contract. The values are read live from the tables, including the DXSteel fields, which the app reads by field name so that it does not depend on the DXSteel app: without Steel 365, a DXSteel text field returns "" and a DXSteel number returns 0.

voaItems

Source table: Item. No prices, costs or inventory are exposed.

PropertyTypeSource
NostringNo.
DescriptionstringDescription
Description2stringDescription 2
BaseUnitofMeasurestringBase Unit of Measure
DXPriceUOMSalesDMXstringDXSteel Price UOM (Sales) DMX
ItemCategoryCodestringItem Category Code
DXItemProfileCodeDMXstringDXSteel Item Profile Code DMX
DXQualityCodeDMXstringDXSteel Quality Code DMX, the grade
BlockedBooleanBlocked
SalesBlockedBooleanSales Blocked

voaVariants

Source table: Item Variant.

PropertyTypeSource
ItemNostringItem No.
CodestringCode
DescriptionstringDescription
Description2stringDescription 2
DXLengthnumberDXSteel Length DMX
DXWidthnumberDXSteel Width DMX
DXThicknessnumberDXSteel Thickness DMX
DXExtraLengthnumberDXSteel Extra Length DMX
DXFormatTypestringDXSteel Format Type DMX, as the caption in the session language. Send a fixed Accept-Language if you map these captions.
BlockedBooleanBlocked
DXRemainingInventorynumberDXSteel Remaining Inventory DMX, summed over all locations

voaCategories

Source table: Item Category, with the properties Code and Description. There is no parent field; the reference website derives the tree from the code (BALK-IPE is under BALK).

voaProfiles, voaGrades, voaItemTypes

These three point at pages of iFacto Steel 365 (DXSteel), not at pages of this app. Their properties are defined by iFacto and can change with iFacto updates; this reference does not specify them. They exist only where Steel 365 is installed.

voaRequests: quote and account requests

Two actions, each taking the whole request as one string parameter payload that contains JSON, and each returning {"number":"…"} as a string. The website account can create requests but cannot read them back: there is no service that lists requests or looks up a customer by e-mail address.

ActionParameterReturns
voaRequests_CreateQuoteRequestpayloadstring: {"number":"WEB000123"}
voaRequests_CreateAccountRequestpayloadstring: {"number":"ACC000045"}

How the payload is read: a missing key and a JSON null both mean empty ("", 0 or false); text values are truncated silently to the field length; Code fields are stored in uppercase; the call runs in one transaction, so if anything fails, nothing is stored. Business Central sets the request number, the status New, the received timestamp, the linked customer and, for account requests, the reviewer and the rejection reason; the payload never does.

CreateQuoteRequest: header keys

KeyTypeRequiredMaxNotes
requestIdstringrecommended64Idempotency key, uppercased. Also the only way LinkRequest can find the request.
privacyAcceptedBooleanyes, trueThe request is refused without it.
privacyConsentVersionstringyes20Which privacy statement was accepted. Uppercased.
contactNamestring100Contact person.
emailstring100Where the requester expects the quote.
companyNamestring100
vatRegistrationNostring20Not verified.
phoneNostring30
customerReferencestring35The customer's own reference. Becomes External Document No. on the quote.
languageCodestring10A Business Central language code, for example NLB or FRB. The confirmation mail is written in this language.
address, address2, postCode, city, countryCodestring100 / 50 / 20 / 30 / 10The requester's billing address. The country is an ISO code, for example BE.
deliveryMethodstringPickup or Delivery. Anything else becomes Pickup.
shipToCodestring10A registered ship-to address of the customer, from MyDeliveryAddresses. Only honoured after LinkRequest verifies it.
useShipToAddressBooleanThe delivery address differs from the billing address.
shipToName, shipToAddress, shipToAddress2, shipToPostCode, shipToCity, shipToCountryCodestring100 / 100 / 50 / 20 / 30 / 10The delivery address as text. A different delivery country can change the VAT treatment of the quote.
requestedDeliveryDatestringYYYY-MM-DD. A value that is not a valid date makes the call fail.
remarksstring2048Free text from the requester.
ipHashstring64See IP hash above.
sourcestring50The channel or page. counterVisit has a special meaning; see below.
assistedBystring50The employee at the counter. Only for counter visits.
quoteRequestLinesarrayLine objects. Missing or not an array: the request has no lines. Elements that are not objects are skipped.

CreateQuoteRequest: line keys

Objects in quoteRequestLines, numbered 10000, 20000, … in array order:

KeyTypeMaxNotes
itemNostring20Not validated on insert; checked when inside sales converts the request. Empty for a free-text line.
variantCodestring10The size.
profileCodestring30Steel 365 profile.
qualityCodestring20Steel 365 grade.
descriptionstring100As shown on the website, or free text.
quantitynumberThe requested quantity.
unitOfMeasureCodestring10For example PCS, M or KG. Validated at conversion.
lengthMmnumber0 means the standard length of the size.
widthMmnumberPlates and strip only.
thicknessMmnumberPlates and strip only.
lineRemarksstring250For example a sawing instruction.

An example payload, before it is serialized into payload:

{
  "requestId": "3F2C9A1E7B0D4C5E",
  "languageCode": "NLB",
  "contactName": "Jan Example",
  "companyName": "Example Ltd",
  "email": "jan@example.com",
  "customerReference": "Site 12",
  "address": "Example Street 1",
  "postCode": "2000",
  "city": "Antwerp",
  "countryCode": "BE",
  "deliveryMethod": "Delivery",
  "useShipToAddress": true,
  "shipToName": "Example Ltd",
  "shipToAddress": "Yard Road 5",
  "shipToPostCode": "2100",
  "shipToCity": "Deurne",
  "shipToCountryCode": "BE",
  "requestedDeliveryDate": "2026-10-01",
  "remarks": "Please call before delivery.",
  "privacyAccepted": true,
  "privacyConsentVersion": "2026-09",
  "ipHash": "<64 hex characters>",
  "source": "shop/en/quote-request",
  "quoteRequestLines": [
    {
      "itemNo": "1000",
      "variantCode": "6000",
      "quantity": 4,
      "unitOfMeasureCode": "PCS",
      "lineRemarks": "Cut to 3 m"
    }
  ]
}

What happens next:

  • The request lands in its own table, not in a sales quote. Inside sales converts it with one action, from the Business Central client only.
  • The mail job sends the requester a confirmation and inside sales a notification, depending on the switches in setup.
  • source = counterVisit marks a request entered at the counter. It converts to a sales order instead of a quote, and no confirmation or notification mail is sent. For a pickup, the order gets a text line naming the employee in assistedBy.
  • If the visitor is signed in, call LinkRequest right after this call to put the session's customer on the request. The customer number is never part of the payload.
  • shipToCode is only trusted after LinkRequest has checked it against the session's customer. On a request that was not linked that way, the conversion refuses the code until inside sales confirms it by hand, so send the address as text as well.

CreateAccountRequest

A request for a portal account. Inside sales links it to a customer and a role, or rejects it.

KeyTypeRequiredMaxNotes
requestIdstringrecommended64Idempotency key, uppercased.
privacyAcceptedBooleanyes, true
privacyConsentVersionstringyes20Uppercased.
emailstringyes100The address the person will sign in with.
contactNamestring100
jobTitlestring50Helps inside sales choose Buyer or Viewer.
companyNamestring100Not verified.
vatRegistrationNostring20
phoneNostring30
address, address2, postCode, city, countryCodestring100 / 50 / 20 / 30 / 10
existingCustomerstringYes, No or Unknown. Anything else becomes Unknown.
statedCustomerNostring20What the person says their customer number is. A hint for inside sales, never a link.
languageCodestring10Used for the mails to this person, including later sign-in codes.
remarksstring2048
ipHashstring64
sourcestring50The page the request came from.

Idempotency: requestId

Generate one requestId per form submission and send the same value again if you retry that submission. A second request with a requestId that already exists is refused with HTTP 400, and the error message contains the marker [VOA-DUBBEL] and the number of the request that was already stored. Treat that as success: the request is in Business Central, only not from this call. The message text itself is translated, so match on the marker, never on the sentence. Comparison is effectively case-insensitive, because the value is stored uppercase; without a requestId there is no duplicate check. The rule holds for both actions, each in its own table.

Errors

All are raised as AL errors, so the call fails with HTTP 400 and the message in the body. The request is not stored.

ConditionApplies toThe message starts with
payload is not valid JSONbothThe request is not valid JSON.
Refuse Requests is onbothCustomerGate is not accepting requests at the moment.
privacyAccepted is not truebothA request cannot be accepted without agreement…
privacyConsentVersion is emptybothInclude in privacyConsentVersion…
email is emptyaccount requestsAn account request without an e-mail address is not possible…
requestId already usedbothThis submission has already been accepted under number… (contains [VOA-DUBBEL])
requestedDeliveryDate is not a valid datequote requestsBusiness Central's own date error

voaPortal: the customer portal

Every action is POST .../ODataV4/voaPortal_{Procedure}?company={company id}. These fifteen procedures are the only callable ones; every other procedure in the codeunit is local.

ProcedureParametersReturnsRoleChanges data
RequestCodeemail, ipHashBooleananyonecreates a code, sends a mail
SignInemail, code, ipHashstring: token or ""anyoneuses the code, creates a session
ReadSessiontokenJSON string or ""bothextends the session
SignOuttokenBoolean (always true)bothrevokes the session
MyDetailstokenJSON string or ""bothextends the session
MyDeliveryAddressestokenJSON string or ""Buyerextends the session
LinkRequesttoken, requestIdBooleanBuyerlinks a request to the customer
MyRequeststokenJSON string or ""bothextends the session
MyQuotestokenJSON string or ""bothextends the session
MyOrderstokenJSON string or ""bothextends the session
MyShipmentstokenJSON string or ""bothextends the session
DocumentLinestoken, documentType, documentNoJSON string or ""bothextends the session
AcceptQuotetoken, quoteNo, ipHashJSON stringBuyerrecords an acceptance
MyDocumentstokenJSON string or ""bothextends the session
Documenttoken, documentType, documentNoJSON string or ""bothextends the session

Hard limits

ListMaximum
MyRequests200 requests
MyQuotes, MyOrders, MyShipments100 rows each, live and archived together; truncated tells you when more existed
MyDeliveryAddresses100 addresses
DocumentLines500 lines
MyDocuments, Steel 365 archive200 documents, from at most 1,000 archive rows examined
MyDocuments, report source100 per document kind

RequestCode, SignIn, SignOut

RequestCode always returns true, after at least 1.5 seconds; do not call it again automatically, because every call can send a mail. SignIn returns the 64-character session token, or "" when the code is wrong, expired or already used, or when the portal user or customer is no longer allowed; the answer never says which. A correct code for a user who has been deactivated in the meantime is used up and returns "". SignOut revokes the session if the token exists and always returns true, also for an unknown token; discard the cookie either way.

ReadSession

Who is behind the token. Call it on every page view of the portal: it checks the session, the portal user and the customer, and extends the session.

KeyTypeNotes
emailstringThe portal user's address.
customerNostringThe customer of this session. Business Central tells the website; the website never tells Business Central.
customerNamestringCustomer name, or "".
rolestringBuyer or Viewer.
weightTypestringThe customer's Steel 365 weight type (Theoretical, Trade, GermanTrade), or "" without Steel 365 or when not set.
expiresAtstringTimestamp when the session expires if it is not used again.

MyDetails

The company details of the session's customer, to prefill the quote request form. Every field is a string and can be "". The customer number is deliberately not included, and there are no prices, discounts or credit data. The keys are company, vatRegistrationNo, address, address2, postCode, city, country, phoneNo, email (the portal user's address, not the customer card's) and role.

MyDeliveryAddresses

The registered ship-to addresses of the session's customer, Buyer only: {"addresses": [{"code", "name", "address", "address2", "postCode", "city", "country"}]}, all strings. A Viewer gets "", just like an invalid session. The billing address is not in this list; it is in MyDetails. Send the chosen code as shipToCode in CreateQuoteRequest.

LinkRequest

Puts the session's customer on a quote request that was just submitted, so call it right after CreateQuoteRequest, with the same requestId. It returns true only if the session is valid, the role is Buyer, requestId (trimmed, uppercased, at most 64 characters) is not empty, and a quote request with that Request ID exists and has no customer yet. It then sets the request's customer, keeps shipToCode only if it is a ship-to address of that customer and otherwise clears it (the address text stays), and sets the request's Source to portal. A false is not an error: the request exists either way and inside sales picks it up like any other.

MyRequests

The quote requests linked to the session's customer, newest first, at most 200. Anonymized requests and requests marked as spam are left out.

KeyTypeNotes
nostringRequest number.
receivedAtstringTimestamp.
statusstringNew, InProgress, Converted, Rejected, or "" for a status this version does not know.
quoteNostringOptional. The sales document created from the request; for a counter visit an order number.
quoteNoStatusstringPresent with quoteNo. active for a live quote of this customer, completed for a released quote in the quote archive, otherwise "".
linesnumberNumber of request lines.

MyQuotes

The sales quotes of the session's customer: first the live quotes, highest number first, then quotes that exist only in the quote archive (converted or deleted), released versions only, the highest archived version per number. Together at most 100 rows. The answer is {"quotes": [ … ], "truncated": <Boolean>}.

KeyTypeNotes
nostringQuote number.
documentDatestringDate.
validUntilstringQuote Valid Until Date, else the Steel 365 quote due date, else "".
yourReferencestringThe customer's reference: External Document No.
requestNostringThe Your Reference field. On quotes created from a web request it holds the request number; on other quotes it can contain anything.
statusstringOpen, Released, or "" for any other status.
linesnumberNumber of item lines.
totalWeightKgnumberOptional, Steel 365 only. Total weight in kg.
hasDocumentBooleantrue only when Document can print this quote. Always false for archived rows.
acceptedBooleanAn acceptance was recorded through the portal.
commentstringLive rows only: the note inside sales wrote for the customer. Never the internal work description.
orderNo, orderNoStatusstringArchived rows, optional: the order this quote became, from the Steel 365 document archive, with active, completed or "". Only link to the order when this is not "".

MyOrders

The sales orders of the session's customer: live orders, highest number first, then orders that exist only in the order archive, highest archived version per number, any status. Together at most 100 rows, as {"orders": [ … ], "truncated": <Boolean>}.

KeyTypeNotes
nostringOrder number.
quoteNo, quoteNoStatusstringThe quote the order came from, with active, completed or "". Archived rows take it from the Steel 365 document archive.
orderDate, requestedDeliveryDate, promisedDeliveryDatestringDates.
yourReferencestringThe customer's reference (External Document No.).
statusstringOpen, Released or "".
completelyShippedBooleanLive: the order's own field. Archived: recalculated per line from posted shipment lines.
linesnumberNumber of item lines.
hasDocumentBooleanAs in MyQuotes.
commentstringLive rows only: the note for the customer.

MyShipments

The posted sales shipments of the session's customer, newest first, at most 100, as {"shipments": [ … ], "truncated": <Boolean>}.

KeyTypeNotes
nostringShipment number.
deliveryDatestringPosting date of the shipment.
orderNo, orderNoStatusstringThe order the shipment belongs to, with active, completed or "".
yourReferencestringThe customer's reference (External Document No.).
shipToAddressobject{"name", "postCode", "city"}. Street and house number are not exposed.
linesnumberNumber of item lines.
hasDocumentBooleanAs in MyQuotes.

DocumentLines

Parameters: documentType is quote, order or shipment (case-insensitive; anything else returns ""), and documentNo is the document number. Ownership is checked before any line is read. For quote and order the live document is tried first; if it does not exist, the highest version in the archive, for quotes released versions only. Only lines of type Item are returned, at most 500; text and comment lines are left out. The answer is {"lines": [ … ]}.

KeyTypeWhenNotes
lineNonumberalwaysLine No.
itemNostringalwaysItem number.
descriptionstringalways
formatstringalwaysVariant Code: the size.
quantitynumberwithout Steel 365Quantity.
unitOfMeasureCodestringwithout Steel 365Unit of measure of quantity.
outstandingQuantity, quantityShippednumberwithout Steel 365, live order lines
profile, grade, formatDescriptionstringSteel 365, optionalItem Profile Code DMX, Quality Code DMX, Item Variant Description DMX.
unitstringSteel 365, optionalDoc. Unit of Measure Code DMX, untranslated option name. Says which of the three quantities leads; the reference website expects PCS, KG or MTR.
quantityPieces, quantityMeters, quantityKgnumberSteel 365Quantity Pieces DMX, Quantity Length DMX, Quantity Weight DMX.
weightTypestringSteel 365, optionalTheoretical, Trade or GermanTrade.
outstandingPieces, outstandingMeters, outstandingKg, shippedPieces, shippedMeters, shippedKgnumberSteel 365, live order linesOutstanding and shipped quantities.

Rules for reading lines: Steel 365 text keys are left out when empty, while Steel 365 number keys are present whenever the field exists, so 0 means zero and a missing key means unknown (show a dash, not 0). With Steel 365, pieces, meters and kilograms are three separate quantities: never add them up or pick one yourself, unit says which one leads. Outstanding and shipped values exist only on live order lines; do not calculate them yourself. No price, discount, margin or amount is ever returned.

{
  "lines": [
    {"lineNo": 10000, "itemNo": "1000", "description": "HEA 200 S235JR", "format": "12000",
     "profile": "HEA200", "grade": "S235JR", "unit": "PCS", "quantityPieces": 4,
     "quantityMeters": 48, "quantityKg": 2035.2, "weightType": "Theoretical"}
  ]
}

AcceptQuote

Records that the customer accepts a released sales quote. Buyer only. Nothing is converted or released in Business Central: the acceptance is a signal for inside sales, who convert the quote by hand. It always returns an object with outcome (recorded, already, expired or refused) and acceptedAt (a timestamp, or ""). The checks, in order:

CheckOutcome
Session not validrefused
Role is not Buyerrefused
quoteNo empty or longer than 20 charactersrefused
No live quote with that number for this customerrefused
Quote status is not Releasedrefused
An acceptance already exists for this quotealready, with the original acceptedAt
The validity date is set and before todayexpired
Another request is recording an acceptance for this quote at the same momentalready, acceptedAt = ""
Otherwiserecorded, acceptedAt = now

refused never says why, and already is not an error: show that the acceptance is in. Do not retry this call automatically. The recorded acceptance stores the quote number, the customer, the portal user's e-mail, the moment, the IP hash, the document date, the amount including VAT, the currency, the number of lines, the latest archive version and a fingerprint of the lines, so it stays clear what was accepted if the quote changes later. The mail job then sends the customer a confirmation and inside sales a notification.

MyDocuments

The PDFs available to the session's customer, as {"documents": [ … ]}, with no truncated flag. The source is one of two, never both: the Steel 365 document archive when DXSteel Doc Archive is installed, otherwise the report source when Portal Documents from Reports is on. If the archive is not installed and the setting is off, the list is always empty. If anything goes wrong reading the archive, the list is empty rather than an error, and the setup card shows why.

Archive rows. A document is listed only if it hangs on this customer's number, the relation type is Customer, and its category is set up in Archive Categories with a kind other than none. Newest first.

KeyTypeNotes
idstringSystemId of the archive record, lowercase with hyphens. The key for downloading.
fileNamestringAs stored in the archive. May contain spaces and accents; sanitize it before using it in a header.
contentTypestringapplication/pdf for .pdf, otherwise application/octet-stream.
categorystringThe kind: SalesQuote, SalesOrder, SalesInvoice, SalesCreditMemo, Proforma, Waybill or WarehouseShipment. Not the archive's own category name.
documentNostringThe source document number, or "".
documentTypestringquote, order, shipment or "". Set only after Business Central confirmed that documentNo is such a document of this customer. Invoices, credit memos and pro formas always get "".
documentNoStatusstringOnly when documentType is order: active or completed.
datestringThe archive's creation date, else the date the record was created, else "".
orderNo, orderNoStatus, invoiceNo, creditMemoNostringOptional. Related document numbers, with active, completed or "" for the order.

Downloading an archive document. Business Central cannot return the bytes through this codeunit without a dependency on the iFacto app, so the website fetches them from iFacto's API with the id:

POST https://api.businesscentral.dynamics.com/v2.0/{tenant}/{environment}/api/ifacto/dxsteel/v1.0/companies({company id})/documentArchives({id})/Microsoft.NAV.Download
{}

-> {"value": "<base64>"}

Only download an id that appears in the MyDocuments list you just fetched for the same session. Never take an id from the browser and pass it on unchecked: the website's account needs read access to the archive to download, and the same archive holds purchase invoices and internal correspondence. VOA CG S365 ARCHIVE limits that access to the allowed categories with a security filter, but the ownership check is MyDocuments. An empty value means the archive record has no file.

Report rows. Only document kinds for which a report is set up (a document layout on the customer card, else Report Selections): live quotes and orders, posted shipments, invoices and credit memos, in that order, each highest number first and at most 100 per kind.

KeyTypeNotes
idstringSystemId of the Business Central document. Informational; download with Document.
sourcestringAlways report. Absent on archive rows: use it to tell the two apart.
reportTypestringquote, order, shipment, invoice or creditMemo. Pass it as documentType to Document.
fileNamestring{category} {document number}.pdf, for example SalesInvoice 103001.pdf.
contentTypestringapplication/pdf.
categorystringSalesQuote, SalesOrder, Waybill (shipments), SalesInvoice or SalesCreditMemo.
documentNostringDocument number. Pass it as documentNo to Document.
documentTypestringquote, order or shipment; "" for invoices and credit memos.
documentNoStatusstringOnly for orders: active.
datestringQuote: document date. Order: order date. Shipment, invoice, credit memo: posting date.
orderNo, orderNoStatus, invoiceNo, creditMemoNostringOptional. Related document numbers.

Document

The PDF of one document, printed with the report Business Central uses for it, and only with the report source: where the Steel 365 archive is installed this always returns "" and you use the archive download above. documentType is quote, order, shipment, invoice or creditMemo (case-insensitive) and documentNo is the number. It returns "" for an invalid session, an unknown type, a document that does not exist or is not this customer's, a document kind without a report, or an empty print. Quotes and orders must be live; archived versions cannot be printed. Otherwise it returns fileName, contentType and base64, the PDF itself.

The report runs with the permissions of the website account (tenant set VOA CG DOCUMENTS). If the report fails, typically because a customer-specific layout reads a table the set does not cover, the call fails with an error that names the object; add that table to the set. A download does not archive the quote or order, but the standard reports do increase No. Printed. hasDocument in MyQuotes, MyOrders and MyShipments is true exactly when the report source is active and a report is set up for that document kind for this customer.

Fixed values

These values are fixed tokens. They are never translated and never depend on the session language. Compare them exactly.

WhereKey or parameterValues
ReadSession, MyDetailsroleBuyer, Viewer
MyRequestsstatusNew, InProgress, Converted, Rejected, ""
MyQuotes, MyOrdersstatusOpen, Released, ""
MyRequests, MyOrdersquoteNoStatusactive, completed, ""
MyQuotes, MyShipments, MyDocumentsorderNoStatusactive, completed, ""
MyDocumentsdocumentNoStatusactive, completed
AcceptQuoteoutcomerecorded, already, expired, refused
DocumentLinesparameter documentTypequote, order, shipment
Documentparameter documentTypequote, order, shipment, invoice, creditMemo
MyDocumentsdocumentTypequote, order, shipment, ""
MyDocumentscategorySalesQuote, SalesOrder, SalesInvoice, SalesCreditMemo, Proforma, Waybill, WarehouseShipment
MyDocumentssourcereport (report rows only)
MyDocumentsreportTypequote, order, shipment, invoice, creditMemo
MyDocuments, DocumentcontentTypeapplication/pdf, application/octet-stream
ReadSession, DocumentLinesweightTypeTheoretical, Trade, GermanTrade, ""
DocumentLinesunitUntranslated option names of the Steel 365 field; the reference website handles PCS, KG, MTR
CreateQuoteRequestdeliveryMethodPickup, Delivery (anything else: Pickup)
CreateQuoteRequestsourceFree text up to 50 characters. counterVisit makes it a counter visit; LinkRequest sets portal.
CreateAccountRequestexistingCustomerYes, No, Unknown (anything else: Unknown)
voaRequests errorsmessage[VOA-DUBBEL] marks a duplicate requestId
Sign-in codealphabet0123456789ABCDEFGHJKMNPQRSTVWXYZ, 10 characters

Request numbers come from number series in setup. On installation they default to WEB000001 to WEB999999 for quote requests and ACC000001 to ACC999999 for account requests, but each company can change them: do not rely on the pattern.

Steel 365 differences

CustomerGate 365 is one app. It checks the environment itself and needs no setting for it: Steel 365 is active when the field Quantity Pieces DMX exists on the sales line, and the Steel 365 document archive is active when the table Document Archive DADMX exists.

AreaWith Steel 365Without Steel 365
Catalog servicesvoaProfiles, voaGrades, voaItemTypes are published, and the DX… properties are filledOnly voaItems, voaVariants, voaCategories; DX… text is "" and numbers are 0
Synchronization permissionsVOA WEB SYNC and VOA CG S365 SYNCVOA WEB SYNC only
DocumentLines quantitiesquantityPieces, quantityMeters, quantityKg plus unit; quantity is never presentquantity plus unitOfMeasureCode
DocumentLines on live order linesoutstandingPieces/Meters/Kg, shippedPieces/Meters/KgoutstandingQuantity, quantityShipped
DocumentLines extra keysprofile, grade, formatDescription, weightType when fillednone
MyQuotes.totalWeightKgpresentabsent
ReadSession.weightTypethe customer's weight type, or """"
Quote validityfalls back to the Steel 365 quote due dateQuote Valid Until Date only

The document archive is a separate switch:

AreaWith the archiveWithout it
MyDocumentsArchive rows, filtered by the allowed categoriesReport rows if Portal Documents from Reports is on, else empty
PDF downloadThe iFacto API bound action with the idDocument returns base64
Documentalways ""prints the PDF
hasDocumentalways falsetrue when a report is set up
Archive-only quotes and ordersorderNo and quoteNo links from the archiveno such links
Website tenant setVOA CG S365 ARCHIVEVOA CG DOCUMENTS when the setting is on

Versioning and compatibility

The names are the contract. Websites call them literally, so each of these is part of the interface: the service names, the procedure and parameter names including their case, the catalog property names, the JSON keys in payloads and responses, and every fixed value above. Renaming one breaks every website built on the app, silently in many cases: a renamed catalog property just leaves a column empty, and a renamed JSON key reads as null. There are no aliases for earlier names.

For consumers:

  • Ignore keys you do not know. New keys can be added in a compatible release.
  • Treat a missing optional key as null, and "" as no value.
  • Treat an unknown token value as unknown. Do not map it to a default that claims something, for example by treating an unknown outcome as recorded.
  • Keep the session token, the requestId and the IP-hash key on the server side.

Adding a key, a procedure or a catalog property is a compatible change; removing or renaming one is not. Installation and updates create missing services but never change an existing one, so a renamed service would appear next to the old one while the old one keeps pointing where it did.

Where to go next

  • Setup guide

    From installing the app to a first request through the customer portal.

  • Release notes

    What is in each version of the app.

  • Support

    How to ask for help, and what to send along.