Integrating External Systems with D365 Finance Using a Custom Web Service

Purpose:

This post outlines the process of creating sales orders in Dynamics 365 for Finance and Operations using a web service. It details an API built to facilitate sales order creation from external systems, including the structure of the data contracts for sales orders, sales lines, and responses, alongside methods for validation and processing within the service.

Application:

Dynamics 365 for Finance and Operations

Business requirement:

Requirement is to create sales order with lines in D365 Finance from an external system in a single API call.

Solution:

Many businesses running Dynamics 365 Finance rely on separate platforms to generate sales orders — billing systems, CRMs, eCommerce platforms, or customer portals — and need those transactions to flow into D365 Finance automatically, without manual re-entry.

This solution delivers a custom web service built directly inside D365 Finance that accepts sales order requests from any external system and handles the full creation process: header data, line items, payment terms, tax amounts, and sales confirmation — all within a single API call.

The service is built with validation at every step, rejecting requests with missing customers, invalid items, or zero-value lines before any data is written. Errors are returned with descriptive messages so the calling system can handle failures cleanly. Successful requests result in a confirmed sales order in D365 Finance, ready for invoicing.

This pattern eliminates manual order entry, reduces the risk of data errors between systems, and gives your integration a reliable, maintainable foundation that scales as transaction volumes grow.

Postman
postman wqfm9rl3e6

Code

Web request
/// <summary>
/// This is the data contract class for the web request for <c>ATLASBillingIntegrationService</c>.
/// </summary>
[DataContractAttribute]
class ATLASBillingIntegrationRequest
{
    private SelectableDataArea dataAreaId;
    private ATLASBillingIntegrationRequestSalesOrder salesOrderRequest;
    private List salesLinesRequest;


    /// <summary>
    /// Gets or sets the value of data contract parameter DataAreaId
    /// </summary>
    /// <param name = "_dataAreaId">
    /// The new value of data contract parameter DataAreaId
    /// </param>
    /// <returns>
    /// The current value of data contract parameter DataAreaId
    /// </returns>
    [DataMember("DataAreaId")]
    public SelectableDataArea parmDataAreaId(SelectableDataArea _dataAreaId = dataAreaId)
    {
        dataAreaId = _dataAreaId;
        return dataAreaId;
    }

    /// <summary>
    /// Gets or sets the value of data contract parameter SalesOrderRequest
    /// </summary>
    /// <param name = "_salesOrderRequest">
    /// The new value of data contract parameter SalesOrderRequest
    /// </param>
    /// <returns>
    /// The current value of data contract parameter SalesOrderRequest
    /// </returns>
    [DataMember("SalesOrderRequest")]
    public ATLASBillingIntegrationRequestSalesOrder parmSalesOrderRequest(ATLASBillingIntegrationRequestSalesOrder _salesOrderRequest = salesOrderRequest)
    {
        salesOrderRequest = _salesOrderRequest;
        return salesOrderRequest;
    }

    /// <summary>
    /// Gets or sets the value of data contract parameter SalesLinesRequest
    /// </summary>
    /// <param name = "_salesLinesRequest">
    /// The new value of data contract parameter SalesLinesRequest
    /// </param>
    /// <returns>
    /// The current value of data contract parameter SalesLinesRequest
    /// </returns>
    [
        DataMember("SalesLinesRequest"),
        DataCollection(Types::Class, classStr(ATLASBillingIntegrationRequestSalesLine)),
        AifCollectionTypeAttribute('_salesLinesRequest', Types::Class, classStr(ATLASBillingIntegrationRequestSalesLine)),
        AifCollectionTypeAttribute('return', Types::Class, classStr(ATLASBillingIntegrationRequestSalesLine))
    ]
    public List parmSalesLinesRequest(List _salesLinesRequest = salesLinesRequest)
    {
        salesLinesRequest = _salesLinesRequest;
        return salesLinesRequest;
    }
}
Web request for sales order
/// <summary>
/// This is the data contract class for the sales order request.
/// </summary>
[DataContractAttribute]
class ATLASBillingIntegrationRequestSalesOrder
{
    private CustAccount custAccount;
    private TaxAmountCur actualSalesTaxAmount;
    private PaymTermId paymTermId;
    private SalesFixedDueDate dueDate;
    private CustRef customerRef;
    private SalesOriginId salesOriginId;


    /// <summary>
    /// Gets or sets the value of data contract parameter CustAccount
    /// </summary>
    /// <param name = "_custAccount">
    /// The new value of data contract parameter CustAccount
    /// </param>
    /// <returns>
    /// The current value of data contract parameter CustAccount
    /// </returns>
    [DataMember("CustAccount")]
    public CustAccount parmCustAccount(CustAccount _custAccount = custAccount)
    {
        custAccount = _custAccount;
        return custAccount;
    }

    /// <summary>
    /// Gets or sets the value of data contract parameter ActualSalesTaxAmount
    /// </summary>
    /// <param name = "_actualSalesTaxAmount">
    /// The new value of data contract parameter ActualSalesTaxAmount
    /// </param>
    /// <returns>
    /// The current value of data contract parameter ActualSalesTaxAmount
    /// </returns>
    [DataMember("ActualSalesTaxAmount")]
    public TaxAmountCur parmActualSalesTaxAmount(TaxAmountCur _actualSalesTaxAmount = actualSalesTaxAmount)
    {
        actualSalesTaxAmount = _actualSalesTaxAmount;
        return actualSalesTaxAmount;
    }

    /// <summary>
    /// Gets or sets the value of data contract parameter PaymentTerms
    /// </summary>
    /// <param name = "_paymTermId">
    /// The new value of data contract parameter PaymentTerms
    /// </param>
    /// <returns>
    /// The current value of data contract parameter PaymentTerms
    /// </returns>
    [DataMember("PaymentTerms")]
    public PaymTermId parmPaymentTerms(PaymTermId _paymTermId = paymTermId)
    {
        paymTermId = _paymTermId;
        return paymTermId;
    }

    /// <summary>
    /// Gets or sets the value of data contract parameter DueDate
    /// </summary>
    /// <param name = "_dueDate">
    /// The new value of data contract parameter DueDate
    /// </param>
    /// <returns>
    /// The current value of data contract parameter DueDate
    /// </returns>
    [DataMember("DueDate")]
    public SalesFixedDueDate parmDueDate(SalesFixedDueDate _dueDate = dueDate)
    {
        dueDate = _dueDate;
        return dueDate;
    }

    /// <summary>
    /// Gets or sets the value of data contract parameter SalesOrigin
    /// </summary>
    /// <param name = "_salesOrigin">
    /// The new value of data contract parameter SalesOrigin
    /// </param>
    /// <returns>
    /// The current value of data contract parameter SalesOrigin
    /// </returns>
    [DataMember("SalesOrigin")]
    public SalesOriginId parmSalesOrigin(SalesOriginId _salesOriginId = salesOriginId)
    {
        salesOriginId = _salesOriginId;
        return salesOriginId;
    }

    /// <summary>
    /// Gets or sets the value of data contract parameter Description
    /// </summary>
    /// <param name = "_customerRef">
    /// The new value of data contract parameter Description
    /// </param>
    /// <returns>
    /// The current value of data contract parameter Description
    /// </returns>
    [DataMember("Description")]
    public CustRef parmCustomerRef(CustRef _customerRef = customerRef)
    {
        customerRef = _customerRef;
        return customerRef;
    }
}
Web request for sales order line
/// <summary>
/// This is the data contract class for the sales order lines request.
/// </summary>
[DataContractAttribute]
class ATLASBillingIntegrationRequestSalesLine
{
    private ItemId itemId;
    private LineAmount lineAmount;
    private TaxAmount taxAmount;
    private ItemFreeTxt itemName;
    private SalesQty salesQty;


    /// <summary>
    /// Gets or sets the value of data contract parameter ItemId
    /// </summary>
    /// <param name = "_itemId">
    /// The new value of data contract parameter ItemId
    /// </param>
    /// <returns>
    /// The current value of data contract parameter ItemId
    /// </returns>
    [DataMember("ItemId")]
    public ItemId parmItemId(ItemId _itemId = itemId)
    {
        itemId = _itemId;
        return itemId;
    }

    /// <summary>
    /// Gets or sets the value of data contract parameter LineAmount
    /// </summary>
    /// <param name = "_lineAmount">
    /// The new value of data contract parameter LineAmount
    /// </param>
    /// <returns>
    /// The current value of data contract parameter LineAmount
    /// </returns>
    [DataMember("LineAmount")]
    public LineAmount parmLineAmount(LineAmount _lineAmount = lineAmount)
    {
        lineAmount = _lineAmount;
        return lineAmount;
    }

    /// <summary>
    /// Gets or sets the value of data contract parameter TaxAmount
    /// </summary>
    /// <param name = "_taxAmount">
    /// The new value of data contract parameter TaxAmount
    /// </param>
    /// <returns>
    /// The current value of data contract parameter TaxAmount
    /// </returns>
    [DataMember("TaxAmount")]
    public TaxAmount parmTaxAmount(TaxAmount _taxAmount = taxAmount)
    {
        taxAmount = _taxAmount;
        return taxAmount;
    }

    /// <summary>
    /// Gets or sets the value of data contract parameter ItemName
    /// </summary>
    /// <param name = "_itemName">
    /// The new value of data contract parameter ItemName
    /// </param>
    /// <returns>
    /// The current value of data contract parameter ItemName
    /// </returns>
    [DataMember("ItemName")]
    public ItemFreeTxt parmItemName(ItemFreeTxt _itemName = itemName)
    {
        itemName = _itemName;
        return itemName;
    }

    /// <summary>
    /// Gets or sets the value of data contract parameter SalesQty
    /// </summary>
    /// <param name = "_salesQty">
    /// The new value of data contract parameter SalesQty
    /// </param>
    /// <returns>
    /// The current value of data contract parameter SalesQty
    /// </returns>
    [DataMember("Quantity")]
    public SalesQty parmSalesQty(SalesQty _salesQty = salesQty)
    {
        salesQty = _salesQty;
        return salesQty;
    }
}
Web response
/// <summary>
/// This is the data contract class for the web response for <c>ATLASBillingIntegrationService</c>.
/// </summary>
[DataContractAttribute]
class ATLASBillingIntegrationResponse
{
    private boolean success;
    private str errorMessage;
    private str debugMessage;


    /// <summary>
    /// Gets or sets the value of data contract parameter Success
    /// </summary>
    /// <param name = "_success">
    /// The new value of data contract parameter Success
    /// </param>
    /// <returns>
    /// The current value of data contract parameter Success
    /// </returns>
    [DataMember("Success")]
    public boolean parmSuccess(boolean _success = success)
    {
        success = _success;
        return success;
    }

    /// <summary>
    /// Gets or sets the value of data contract parameter ErrorMessage
    /// </summary>
    /// <param name = "_errorMessage">
    /// The new value of data contract parameter ErrorMessage
    /// </param>
    /// <returns>
    /// The current value of data contract parameter ErrorMessage
    /// </returns>
    [DataMember("ErrorMessage")]
    public str parmErrorMessage(str _errorMessage = errorMessage)
    {
        errorMessage = _errorMessage;
        return errorMessage;
    }

    /// <summary>
    /// Gets or sets the value of data contract parameter DebugMessage
    /// </summary>
    /// <param name = "_debugMessage">
    /// The new value of data contract parameter DebugMessage
    /// </param>
    /// <returns>
    /// The current value of data contract parameter DebugMessage
    /// </returns>
    [DataMember("DebugMessage")]
    public str parmDebugMessage(str _debugMessage = debugMessage)
    {
        debugMessage = _debugMessage;
        return debugMessage;
    }
}
Web service
/// <summary>
/// This is the service class for <c>ATLASBillingIntegrationService</c>.
/// </summary>
class ATLASBillingIntegrationService
{
    #OCCRetryCount
    #define.SalesOrigin('ExtBillingSystem')


    /// <summary>
    /// Creates sales order and sales invoice
    /// </summary>
    /// <param name = "_request">
    /// Web request
    /// </param>
    /// <returns>
    /// Web response
    /// </returns>
    public ATLASBillingIntegrationResponse run(ATLASBillingIntegrationRequest _request)
    {
        ATLASBillingIntegrationResponse response;
        ATLASBillingIntegrationRequestSalesOrder salesOrderRequest;
        List salesLinesRequest;
        SelectableDataArea dataAreaId;
        SalesId salesId;
        InvoiceDate invoiceDate;
        str infologMessages;

        dataAreaId = _request.parmDataAreaId();
        invoiceDate = today();
        salesOrderRequest = _request.parmSalesOrderRequest();
        salesLinesRequest = _request.parmSalesLinesRequest();
        response = new ATLASBillingIntegrationResponse();

        this.validateWebRequest(_request);

        changecompany(dataAreaId)
        {
            try
            {
                ttsbegin;
                salesId = this.createSalesOrder(salesOrderRequest);
                this.createSalesOrderLines(salesId, salesLinesRequest);
                this.postSalesConfirmation(salesId, invoiceDate);
                ttscommit;

                response.parmSuccess(true);

                if (response.parmSuccess())
                {
                    response.parmDebugMessage(strFmt("@ATLAS:SalesOrderCreated", salesId, dataAreaId));
                }
                else
                {
                    response.parmErrorMessage(strFmt("@ATLAS:ErrorOccurred"));
                }
            }
            catch (Exception::Deadlock)
            {
                retry;
            }
            catch (Exception::UpdateConflict)
            {
                if (appl.ttsLevel() == 0)
                {
                    if (xSession::currentRetryCount() >= #RetryNum)
                    {
                        throw Exception::UpdateConflictNotRecovered;
                    }
                    else
                    {
                        retry;
                    }
                }
                else
                {
                    throw Exception::UpdateConflict;
                }
            }
            catch (Exception::Error)
            {
                infologMessages = this.getInfologMessages();
                response.parmErrorMessage(strFmt("%1", infologMessages));
            }
        }

        return response;
    }

    /// <summary>
    /// Gets infolog messages
    /// </summary>
    /// <returns>
    /// Infolog messages string value
    /// </returns>
    private str getInfologMessages()
    {
        SysInfologEnumerator enumerator;
        ListEnumerator le;
        List messageList;
        str allMessages;

        enumerator = SysInfologEnumerator::newData(infolog.infologData());
        messageList = new List(Types::String);
        allMessages = "";

        while (enumerator.moveNext())
        {
            messageList.addEnd(enumerator.currentMessage());
        }

        infolog.clear();
        le = messageList.getEnumerator();

        while (le.moveNext())
        {
            if (allMessages == "")
            {
                allMessages = le.current();
            }
            else
            {
                allMessages += " " + le.current();
            }
        }

        return allMessages;
    }

    /// <summary>
    /// Validates web request
    /// </summary>
    /// <param name = "_request">
    /// Web request
    /// </param>
    private void validateWebRequest(ATLASBillingIntegrationRequest _request)
    {
        ATLASBillingIntegrationRequestSalesOrder salesOrderRequest;
        ATLASBillingIntegrationRequestSalesLine salesLineRequest;
        ListIterator listIterator;
        List salesLinesRequest;
        SelectableDataArea dataAreaId;
        CustAccount custAccount;
        PaymTermId paymTermId;
        ItemId itemId;
        LineAmount lineAmount;
        SalesOriginId salesOriginId;
        SalesQty salesQty;

        dataAreaId = _request.parmDataAreaId();
        salesOrderRequest = _request.parmSalesOrderRequest();
        salesLinesRequest = _request.parmSalesLinesRequest();
        custAccount = salesOrderRequest.parmCustAccount();
        paymTermId = salesOrderRequest.parmPaymentTerms();
        salesOriginId = salesOrderRequest.parmSalesOrigin();

        if (!CompanyInfo::findDataArea(dataAreaId))
        {
            throw error(strFmt("@ATLAS:CompanyDoesNotExist", dataAreaId));
        }

        changecompany(dataAreaId)
        {
            if (!CustTable::exist(custAccount))
            {
                throw error(strFmt("@ATLAS:CustomerDoesNotExist", custAccount));
            }

            if (!PaymTerm::exist(paymTermId))
            {
                throw error(strFmt("@ATLAS:PaymentTermDoesNotExist", paymTermId));
            }

            if (!SalesOrigin::exist(salesOriginId))
            {
                throw error(strFmt("@ATLAS:SalesOriginDoesNotExist", salesOriginId));
            }

            if (salesLinesRequest)
            {
                listIterator = new ListIterator(salesLinesRequest);

                while (listIterator.more())
                {
                    salesLineRequest = listIterator.value();
                    itemId = salesLineRequest.parmItemId();
                    lineAmount = salesLineRequest.parmLineAmount();
                    salesQty = salesLineRequest.parmSalesQty();

                    if (!InventTable::exist(itemId))
                    {
                        throw error(strFmt("@ATLAS:ItemDoesNotExist", itemId));
                    }

                    if (lineAmount == 0)
                    {
                        throw error(strFmt("@ATLAS:LineAmountNotProvided", salesLineRequest.parmItemId()));
                    }

                    if (salesQty == 0)
                    {
                        throw error(strFmt("@ATLAS:QuantityNotProvided", salesLineRequest.parmItemId()));
                    }

                    listIterator.next();
                }
            }
        }
    }

    /// <summary>
    /// Creates sales order header
    /// </summary>
    /// <param name = "_salesOrderRequest">
    /// Sales order details
    /// </param>
    /// <returns>
    /// Sales order number
    /// </returns>
    private SalesId createSalesOrder(ATLASBillingIntegrationRequestSalesOrder _salesOrderRequest)
    {
        CustAccount custAccount;
        SalesTable salesTable;
        PaymTermId paymTermId;
        SalesOriginId salesOriginId;
        CustRef customerRef;
        NumberSeq numberSeq;

        custAccount = _salesOrderRequest.parmCustAccount();
        salesOriginId = _salesOrderRequest.parmSalesOrigin();
        customerRef = _salesOrderRequest.parmCustomerRef();
        paymTermId = PaymTerm::find(_salesOrderRequest.parmPaymentTerms()).PaymTermId;
        numberSeq = NumberSeq::newGetNum(SalesParameters::numRefSalesId());
        numberSeq.used();

        salesTable.SalesId = numberSeq.num();
        salesTable.initValue();
        salesTable.CustAccount = custAccount;
        salesTable.initFromCustTable();
        salesTable.SalesOriginId = SalesOrigin::find(salesOriginId).OriginId;
        salesTable.CustomerRef = customerRef;
        salesTable.Payment = paymTermId;
        salesTable.FixedDueDate = _salesOrderRequest.parmDueDate();

        if (!salesTable.validateWrite())
        {
            throw Exception::Error;
        }

        salesTable.insert();

        return salesTable.SalesId;
    }

    /// <summary>
    /// Creates sales order lines
    /// </summary>
    /// <param name = "_salesId">
    /// Sales order number
    /// </param>
    /// <param name = "_salesLinesRequest">
    /// List of sales order lines
    /// </param>
    private void createSalesOrderLines(SalesId _salesId, List _salesLinesRequest)
    {
        ATLASBillingIntegrationRequestSalesLine salesLineRequest;
        ListIterator listIterator;
        SalesTable salesTable;
        SalesLine salesLine;
        ItemId itemId;
        SalesQty salesQty;
        LineAmount lineAmount;
        TaxAmount taxAmount;
        ItemFreeTxt itemName;

        if (_salesLinesRequest)
        {
            listIterator = new ListIterator(_salesLinesRequest);

            while (listIterator.more())
            {
                salesLineRequest = listIterator.value();
                
                salesTable = SalesTable::find(_salesId);
                itemId = salesLineRequest.parmItemId();
                lineAmount = salesLineRequest.parmLineAmount();
                taxAmount = salesLineRequest.parmTaxAmount();
                itemName = salesLineRequest.parmItemName();
                salesQty = salesLineRequest.parmSalesQty();

                salesLine.clear();
                salesLine.initFromSalesTable(salesTable);
                salesLine.SalesId = salesTable.SalesId;
                salesLine.ItemId = itemId;
                salesLine.SalesQty = salesQty;
                salesLine.createLine(true, true, true, true, true, true);

                salesLine.SalesPrice = lineAmount;
                salesLine.modifiedField(fieldNum(SalesLine, SalesPrice));
                salesLine.ATLASTaxAmount = taxAmount;
                salesLine.Name = itemName;
                salesLine.update();

                listIterator.next();
            }
        }
    }

    /// <summary>
    /// Posts sales order confirmation
    /// </summary>
    /// <param name = "_salesId">
    /// Sales order number
    /// </param>
    private void postSalesConfirmation(SalesId _salesId, TransDate _transDate)
    {
        SalesFormLetter salesFormLetter;
        SalesTable salesTable;

        salesTable = SalesTable::find(_salesId);
        salesFormLetter = SalesFormLetter::construct(DocumentStatus::Confirmation);
        salesFormLetter.update(salesTable, _transDate);
    }
}

Need This Built for Your Business?

If your team is managing sales order creation manually between systems, or your current integration isn’t reliable enough to trust at scale, we can help.

Atlas Dynamics specializes in building custom D365 Finance integrations that are production-ready, properly validated, and designed to last. Whether you need to connect a billing platform, CRM, eCommerce system, or internal portal to D365 Finance, our team delivers end-to-end — from scoping and development through to testing and go-live support.

Get in touch

Get in touch to discuss your integration requirements. We’ll assess your scenario and outline what a solution looks like for your environment.

Leave a Reply

Scroll to Top

Discover more from Atlas Dynamics Consulting

Subscribe now to keep reading and get access to the full archive.

Continue reading