Inserting, updating, and deleting data with the Easysoft Xero ODBC driver

We're currently adding INSERT, UPDATE, and DELETE support to the Xero ODBC driver. For a pre-release version of the read-write Xero ODBC driver, contact the Easysoft Support team ().

Contents

Which tables support INSERT, UPDATE, and DELETE statements?

You can query any table exposed by the Xero ODBC driver. For INSERT, UPDATE, or DELETE support, refer to this table:

Table Name INSERT UPDATE DELETE
Accounts
BankTransactions
BankTransfers
BatchPayments 2
BrandingThemes 3
Contacts
ContactGroups 1
CreditNotes 2
Currencies
Employees
ExpenseClaims
Invoices
Items
LinkedTransactions
ManualJournals
Payments
PurchaseOrders
Quotes
Receipts
RepeatingInvoices
TaxRates
TrackingCategories

Notes

  1. You can't currently update the Contacts column in the ContactGroup tables.
  2. You can't delete BatchPayments or CreditNotes records but you can change their status to DELETED.
  3. You can use the Xero XPI to apply BrandingThemes records to a PaymentServices record.

Special tables

LastId table

Use the LastId table to find out the status of your most recent INSERT, UPDATE, or DELETE statement.

Column Name Description
LastID The ID of the last Xero record to be affected by an INSERT, UPDATE, or DELETE statement.
Status XML status response.
LastHTTPCode HMTL response code. For example:
200: OK
404: Not Found
LastHTTPMessage HTTP message.

For example:

UPDATE Contacts SET EmailAddress='example_user@example.com' WHERE [Name]='example_customer'
SELECT LastID FROM LastId
LastID: a166e4c6-1a89-4d5e-acf3-939a9f82e23c

Note INSERTs always update the LastID table. UPDATEs and DELETEs only change the LastID table if a record is succesfully updated or deleted. For example, if you ran the UPDATE statement shown earlier twice, the second UPDATE would not alter the data and therefore not update LastID.

The LastId table and columns only apply to the current connection. For example, if you connect, insert a record, disconnect, and then re-connect, the LastID table is empty.

If your SQL statement contains a value that's not valid, LastID contains the reason why the statement failed. For example:

UPDATE
   BankTransactions 
SET
   LineItems = replace(LineItems, '<Quantity>1.0000</Quantity>', '<Quantity>2</Quantity>') 
WHERE
   BankTransactionId = '737982b0-2811-44c9-bdb3-3b26a3a6ef8c' -- invalid BankTransactionId
SELECT * from LastId

produces this output:

Column Name Value returned Description
LastID NULL Nothing has been updated.
Status NULL
LastHTTPCode 404 Xero API says your query did not work.
LastHTTPMessage Last HTTP message. The BankTransactionId was not valid.

History table

Use the History table to query the Xero history for:

The History table contains these columns:

Column Name Description
Endpoint The History endpoint.
Guid The ID of the record being read.
Changes The change made. For example, "Created" or "Edited."
DateUTCString Timestamp in a string format.
DateUTC Timestamp in an ODBC SQL_TYPE_TIMESTAMP format.
User The Xero user who made the changes.
Details Futher information about what changes were made.

Pass the endpoint and GUID in your WHERE clause. For example:

SELECT
   * 
FROM
   history 
WHERE
   endpoint = 'Contacts' 
   AND guid = '9ce626d2-14ea-463c-9fff-6785ab5f9bfb'

The Details column is a LONGVARCHAR type. To sort the results by Details, convert the column to a VARCHAR type. For example:

SELECT
   Changes,
   DateUTC,
   CAST(Details as varchar(512)) AS & quot;
Desc & quot;
FROM
   history 
WHERE
   endpoint = 'Contacts' 
   AND guid = '9ce626d2-14ea-463c-9fff-6785ab5f9bfb' 
ORDER BY
   DateUTC DESC

How do I get the GUID?

The GUID is the same as the ID of the primary key in the table, so, for example:

SELECT QuoteId from Quotes

returns all the GUIDs from the Quotes table.

API table

Use this table to query the Xero API.

Column Name Direction Reguired Description
URL Input Yes A Xero API endpoint. For example, Invoices or Accounts.
Method Input Yes GET, PUT, POST, or DELETE depending on which Xero API you are calling.
Code Output Not applicable HMTL response code. For example:
200: OK
404: Not Found
XMLRequestBody Input No The XML to send to the Xero API. Use URL encoded strings.
JSONRequestBody Input No The JSON to send to the Xero API.
Response Output Not applicable HTML response text.
XMLResponseBody Output Not applicable The XML response from the Xero API.

Example queries:

SELECT * FROM api WHERE url = 'Currencies' AND Method = 'GET';
	      
SELECT
   * 
FROM
   api 
WHERE
   url = 'Accounts/524c9f2d-e680-4861-82f6-4ad171b51de5' 
   AND Method = 'GET';
SELECT
   * 
FROM
   api 
WHERE
   url = 'Currencies' 
   AND Method = 'PUT' 
   AND XMLRequestBody = '<Currency><Code>SGD</Code></Currency>';
SELECT
   * 
FROM
   api 
WHERE
   url = 'ContactGroups/fd33e5d2-3f95-4b1b-a405-7ed7067b9c22/Contacts' 
   AND Method = 'DELETE'

If you send invalid XML in the XMLRequestBody:

SELECT
   * 
FROM
   api 
WHERE
   url = 'Currencies' 
   AND Method = 'PUT' 
   AND XMLRequestBody = '<Currency2><Code>SGD</Code></Currency>'

the Xero API return code is 400, the response is "Bad Request" and the XMLResponseBody is the relevant API error response:

<ApiException xmlns:xsd="http://www.w3.org/2001/XMLSchema"
	      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <ErrorNumber>14</ErrorNumber>
    <Type>PostDataInvalidException</Type>
    <Message>Xml for post data was invalid, The 'Currency2' start tag
    on line 1 position 2 does not match the end tag of 'Currency'.
    Line 1, position 30.</Message>
</ApiException>

Warning PUT and POST work in different ways in the Xero API. Check the Xero XPI documentation carefully to be absolutely sure that you are using the correct method when inserting or updating records.

JSONRequestBody columns

From the Xero API Development team:

"At Xero we use .NET, and used the Microsoft .NET JSON date format available at the time of original development. We know it's ugly but not something we can fix without a breaking change or a new version of the API. We're really sorry about this. To help you along, below we explain how to handle this date format. An example date/time in JSON is returned like this: https://developer.xero.com/documentation/api/accounting/requests-and-responses."

Here is an example query that uses JSON in the WHERE clause:

SELECT
   * 
FROM
   api 
WHERE
   url = 'TrackingCategories/201a3021-77de-495d-a233-1faaa40e2f64/Options' 
   AND Method = 'PUT' 
   AND JSONRequestBody = '{ "Name": "New Track 5" }'

Note You can't update the BankTransfer table.

XML format columns

Some Xero columns need to be in XML format. For example, invoice line items:

<LineItems>
    <LineItem>
        <Description>Retainer for consulting work</Description>
        <UnitAmount>500.00</UnitAmount>
        <AccountCode>200</AccountCode>
        <Quantity>3</Quantity>
        <AccountID>c563b607-fb0e-4d06-9ddb-76fdeef20ae3</AccountID>
    </LineItem>
</LineItems>

You can use spaces and newlines in your XML to improve readability if you prefer, but you don't need to.

You don't need to include all XML fields. In the example, the field <LineAmount> is missing. Xero automatically calculates the <LineAmount> value and puts that in for you using the formula:

LineAmount = Quantity * Unit Amount * ((100 – DiscountRate)/100)

Encode reserved charaters within your XML tags. For example:

Not encoded This & that value are < the other.
URL encoded This%20%26%20that%20value%20are%20%3C%20the%20other%2E
HTML entities This &amp; that value are &lt; the other.

The Xero API always replaces reserved characters with HTML entities when returning data. For INSERTs and UPDATEs, use either URL encoding, entities, or a combination of both.

If you get the error "[Easysoft ODBC-Xero Driver]Invalid XML detected," check your data for reserved characters.

XML element names are case sensitive. For example, <ContactId> is not valid, <ContactID> is. If you don't use the correct case, Xero returns an error similar to the following:

"PostDataInvalidException: The element 'ContactId' was not recognised. Ensure the element name has the correct case and that there are no duplicate elements of the same name."

Using Microsoft Visual Basic for Applications (VBA) to URL encode data

Public Function EncodeHex(byVal sInput As String) As String
    Dim TempAns As String
    Dim CurChr As Long
    Dim x As Variant
          
    CurChr = 1
    Do Until CurChr - 1 = Len(sInput)
        Select Case Asc(Mid(sInput, CurChr, 1))
            Case 48 To 57, 65 To 90, 97 To 122
                TempAns = TempAns & Mid(sInput, CurChr, 1)
            Case Else
                x = Hex(Asc(Mid(sInput, CurChr, 1)))
                If Len(CStr(x)) = 1 Then x = "0" & x
                TempAns = TempAns & "%" & x
        End Select
          
        CurChr = CurChr + 1
    Loop
          
    EncodeHex = TempAns
          
End Function

Using VBA to replace reserved characters with HTML entities

To run this code, add a reference to the "Microsoft HTML Object Library" in the VBA Editor.

Public Function EncodeHTML(ByVal sInput As String) As String
    On Error GoTo ErrHandle
    
    Dim HTMLDoc As New MicrosoftHTML.HTMLDocument
    HTMLDoc.Body.innerText = sInput
    EncodeHTML = HTMLDoc.Body.innerHTML
    
GoTo AllDone

ErrHandle:
    MsgBox "URLEncodeHTML error has occurred" & vbCrLf & vbCrLf & _
    "Error Source: EncodeHTML " & vbCrLf & _
    "Error Number: " & Err.Number & vbCrLf & _
    "Error Description: " & Err.Description, vbCritical, "An Error has Occurred!"
    
AllDone:
    On Error Resume Next
    Set oHTMLFile = Nothing
End Function

Using SQL Server to replace reserved characters with HTML entities

SELECT 'This & that value are < the other.' FOR XML PATH('')

Returns:

This &amp; that value are &lt; the other.

Tables with XML format columns

Table Column
BankTransactions LineItems
Contacts Addresses
Phones
Persons
ContactGroups Contacts
CreditNotes LineItems
Allocations
ExpenseClaims Receipts
Payments
Invoices LineItems
Payments
CreditNotes
Prepayments
Overpayments
ManualJournals JournalLines
Organisation Addresses
Phones
ExternalLinks
Prepayments LineItems
Allocations
PurchaseOrders LineItems
Quotes LineItems
Receipts LineItems
RepeatingInvoices LineItems
TaxRates TaxComponents
TrackingCategories Options

Attachments

Use the Xero ODBC driver to retrieve attachments from these tables:

Example: downloading an attachment

This VBA code sample downloads an invoice attachment. The attachment is a JPEG file.

Sub GETInvoiceImage()
    Dim c As New ADODB.Connection
    Dim r As New ADODB.Recordset
    Dim st As New ADODB.Stream
    Dim arInputByte() As Byte
    Dim FName As String
    Dim i As Integer
          
    FName = "c:\l\WindMill3.jpg"
    
    c.Open "XeroDemo"
    r.Open "SELECT Code, RAWResponseBody from api where url = " _
       & "'Invoices/4953d237-6cf2-4ef4-8f37-044aada0d594/Attachments/WindMill3.jpg'" _
       & "and Method = 'GET'", c

    If Not r.EOF And r("Code") = 200 Then
        st.Type = adTypeBinary
        st.Open
        st.Write r(1)
        st.SaveToFile FName
        st.Close
        Debug.Print "File Saved"
    Else
        Debug.Print "File not found."
    End If

    r.Close
    c.Close
    
    Set r = Nothing
    Set c = Nothing
End Sub

Example: uploading an attachment

Use the API table to upload files. This VBA code sample uploads an invoice attachment.

Sub PUTInvoiceImage()
    Dim c As New ADODB.Connection
    Dim cm As New ADODB.Command
    Dim st As New ADODB.Stream
    Dim SQL As String
    Dim FName As String
    Dim pos As Long
    
    FName = "c:\l\WindMill8.jpg"
    
    SQL = "INSERT INTO api ( url, method, rawrequestbody ) " _
        & " VALUES( 'Invoices/4953d237-6cf2-4ef4-8f37-044aada0d594/Attachments/"
    pos = 0

    Do While InStr(pos + 1, FName, "\", vbBinaryCompare) > 0
        pos = InStr(pos + 1, FName, "\", vbBinaryCompare)
    Loop

    SQL = SQL & Right(FName, Len(FName) - pos)
    SQL = SQL & "', 'PUT', ? )"

    Debug.Print SQL
    
    c.Open "XeroDemo"
    
    st.Type = adTypeBinary
    st.Open
    st.LoadFromFile FName

    With cm
        .ActiveConnection = c
        .CommandText = SQL
        .Parameters.Append .CreateParameter("RawRequestBody", adLongVarBinary, _
             adParamInput, st.Size, st.Read)
        .Execute
    End With

    Debug.Print "Uploaded"
    
    c.Close
    st.Close
    
    Set c = Nothing
End Sub

Online invoice and credit note attachments

Use this query string:

?IncludeOnline=true

to include your attachment in online invoices and credit notes. For example:

SQL = "
INSERT INTO
   api ( url, method, rawrequestbody ) 
VALUES
   (
      'Invoices/4953d237-6cf2-4ef4-8f37-044aada0d594/Attachments/WindMill8.jpg?IncludeOnline=true' 
   )
    "

Further information

The Xero Invoice documentation says:

"You can upload up to 10 attachments (each up to 3 MB in size) per invoice, once the invoice has been created in Xero."

Exceeding this limit generates the error:

"[Easysoft ODBC-Xero Driver]ValidationException: A validation exception occurred. The file couldn't be uploaded because the API limit of 10 attachments has been exceeded for the document."

You can upload files that are greater than 3 MB in size (in our test, WindMill.jpg was 5.5 MB). However, if you try to do this you may get:

"[Easysoft ODBC-Xero Driver]PayloadOversizeException: This request exceeds the maximum size limit. Refer to the documentation at https://developer.xero.com/documentation/oauth2/limits"

Or, if the Xero API ignores the request and returns a NULL response to the Xero ODBC driver:

"[Easysoft ODBC-Xero Driver]server response 0 (null)"

Example: listing all invoice attachments

Sub GetListOfAttachmentsForAllInvoices()
    Dim c As New ADODB.Connection
    Dim i As New ADODB.Recordset
    Dim r As New ADODB.Recordset
    Dim SQL As String
    Dim RB As String
    Dim XMLDoc As New MicrosoftXML2.DOMDocument60
    Dim XMLNode
    Dim XMLNodes
    Dim XMLString As String
    
    c.Open "XeroDevDemo"
    
    i.Open "SELECT InvoiceId from Invoices where HasAttachments <> 0", c
    
    Do While Not i.EOF
    
        SQL = "SELECT Code, XMLResponseBody FROM API WHERE URL='Invoices/" & i("InvoiceID") _
            & "/Attachments/' AND Method='GET'"
        
        r.Open SQL, c
        If r("Code") = 200 Then
            XMLString = r("XMLResponseBody")
            XMLDoc.LoadXML XMLString
            If XMLDoc.parseError <> 0 Then
                Debug.Print XMLDoc.parseError.reason
            Else
            Set XMLNodes = XMLDoc.getElementsByTagName("FileName")
            If XMLNodes.length <> 0 Then Debug.Print "Invoice Id: " & i("InvoiceID")
                For Each XMLNode In XMLNodes
                    Debug.Print " " & XMLNode.Text
                    DoEvents
                Next
            End If
        End If
        
        r.Close
        i.MoveNext
    
    Loop

    i.Close
    c.Close
    
    Set r = Nothing
    Set i = Nothing
    Set c = Nothing
End Sub

If you have a lot of invoice attachments, this code could take some time to complete.

Accounts table

Supports INSERT, UPDATE, and DELETE statements.

Microsoft SQL Server linked server examples: INSERT, UPDATE, and DELETE statements

This SQL statement updates an accounts description:

EXEC('UPDATE Accounts SET Description=''Expenses incurred in relation to training staff2''
WHERE AccountId=''87e2c007-4a7c-43d7-a19d-b82dcf529313''') AT Xero

To check whether the update succeeded, run this query straight after:

EXEC('SELECT * FROM LastId') AT Xero

This statement creates a new account:

EXEC('INSERT INTO Accounts ( Code, Name, Type, Description, TaxType )
VALUES (''007'', ''Buchanan'' ,''OVERHEADS'', ''Steven Buchanan'', ''NONE'')') AT Xero

This statement deletes the account:

EXEC('DELETE FROM Accounts WHERE Code=''007''') AT Xero

BankTransactions table

Supports INSERT and UPDATE statements.

SQL INSERT example

INSERT INTO
   BankTransactions ( Type, BankAccount_Code, Contact_Name, [Date], CurrencyCode, LineItems ) 
values
   (
      'SPEND', '090', 'Espresso 31', '2024-02-08', 'USD',
      '<LineItems><LineItem><UnitAmount>50.00</UnitAmount>
       <TaxType>INPUT2</TaxType><TaxAmount>8.33</TaxAmount><LineAmount
       >50.00</LineAmount><AccountCode>429</AccountCode><Quantity>
       1.0000</Quantity><LineItemID>e8059c2b-8fe5-4522-9a2f-40d352c4a80f</LineItemID>
       <AccountID>f96c9458-d724-47bf-8f74-a9d5726465ce</AccountID></LineItem></LineItems>'
   )

If the currency code you specify does not match that of the underlying bank account, it will be ignored. In the example, "USD" was specifed but the record's currency code is "GBP."

BankTransactionID", "Type", "ModifiedAfter", "BankAccount_AccountID", "BankAccount_Code",
"BankAccount_Name", "Contact_ContactID", "Contact_Name", "Contact_Status", "Date",
"Status", "LineAmountTypes", "LineItemCount", "SubTotal", "TotalTax", "Total",
"UpdatedDateUTC", "CurrencyCode", "IsReconciled", "HasAttachments", "LineItems"
"db19b6bf-a278-41ff-a0a8-438dd4aa338d", "SPEND", <Null>, "bd9e85e0-0478-433d-ae9f-0b3c4f04bfe4",
 "090", "Business Bank Account", "c82900a5-064c-46e1-9d8b-86404c6bfd01", "Espresso 31",
"ACTIVE", 2024-02-08, "AUTHORISED", "Inclusive", 1, 41.67, 8.33, 50, 2024-02-08 08:42:43,
"GBP", 0, 0, "<LineItems><LineItem><UnitAmount>50.00</UnitAmount><TaxType>INPUT2</TaxType>
              <TaxAmount>8.33</TaxAmount><LineAmount>50.00</LineAmount><AccountCode>429</AccountCode>
              <Quantity>1.0000</Quantity><LineItemID>ee0ad9fe-6c0f-40d5-a1f0-4578540af34c</LineItemID>
              <AccountID>f96c9458-d724-47bf-8f74-a9d5726465ce</AccountID></LineItem></LineItems>"

SQL UPDATE example

UPDATE BankTransactions SET LineItems='<LineItems><LineItem><Description>Petrol in
                                       company car</Description><UnitAmount>59.00</UnitAmount>
                                       <TaxType>INPUT2</TaxType><TaxAmount>11.80</TaxAmount>
                                       <AccountCode>449</AccountCode><Quantity>2.0000</Quantity>
                                       <LineItemID>283b0044-7ebc-482e-b3b7-b58ef6812226</LineItemID>
                                       <AccountID>0be1631e-cc7e-4c27-951f-308c3307c0fe</AccountID></LineItem>
                                       </LineItems>' WHERE BankTransactionId='db7dcb47-4fb1-4190-9f56-911d7ee5c560'

Note You can't update BankTransactions that have the DELETED status or that have not been reconciled.

BankTransfers table

Supports INSERT statements.

SQL INSERT example

INSERT INTO
   BankTransfers ( [Date], FromBankAccountCode, ToBankAccountCode, Amount, CurrencyRate ) 
values
   (
      '2024-02-07', '091', '090', 1, 1
   )

This example transfers 1 GBP from Account 091 to 090 at a rate of 1.

BatchPayments table

Supports UPDATE statements. Use this to set Xero batch payment status to DELETED.

SQL UPDATE example

Sets the specified batch payment status to DELETED:

UPDATE BatchPayments SET Status='DELETED' WHERE BatchPaymentId= '4f7d624a-1899-469e-8121-2dc739fda3f7'

BrandingThemes table

Supports INSERT statements.

Example: apply branding theme to a payment service by using JSON

SELECT
   * 
FROM
   api 
WHERE
   url = 'BrandingThemes/201a3021-77de-495d-a233-1faaa40e2f64/PaymentService' 
   AND Method = 'PUT' 
   AND JSONRequestBody = ' { "PaymentServices": [{ "PaymentServiceID": "de5c978d-3cbf-4ebb-9ca9-20d7cb196ab1" }] }'

Contacts tables

Supports INSERT and UPDATE statements.

Example: adding a contact

This sample code inserts a single contact who has one address and two phone numbers. Use XML to specify the address and phone number.

Sub AddContact()
    Dim c As New ADODB.Connection
    Dim r As New ADODB.Recordset
    Dim e As ADODB.Error
    Dim cmdADO As New ADODB.Command
    Dim prmText As New ADODB.Parameter
    Dim i As Integer
    
    Dim SQL As String
    Dim Phones As String
    Dim Addresses As String

    Addresses = "<Addresses><Address><AddressType>POBOX</AddressType><" _
        & "AddressLine1>P O Box 3955</AddressLine1><AddressLine2>South Mailing Centre" _
        & "</AddressLine2><City>Oaktown</City><PostalCode>OK12 3TN</PostalCode>" _
        & "<AttentionTo>Human Resources Manager</AttentionTo></Address><Address><AddressType" _
        & ">STREET</AddressType></Address></Addresses>"
    Phones = "<Phones><Phone><PhoneType>DDI</PhoneType></Phone><Phone><PhoneType>DEFAULT" _
        & "</PhoneType><PhoneNumber>9191</PhoneNumber><PhoneAreaCode>555</PhoneAreaCode>" _
        & "<PhoneCountryCode>01</PhoneCountryCode></Phone><Phone><PhoneType>FAX</PhoneType>" _
        & "</Phone><Phone><PhoneType>MOBILE</PhoneType></Phone></Phones>"
    
    SQL = "INSERT INTO Contacts ( Name, Addresses, Phones ) VALUES ('example_customer',?,?)"
    
    On Error GoTo ErrHandle
    
    c.Open XeroDemo
    cmdADO.ActiveConnection = c
    cmdADO.CommandText = SQL
    Set prmText = cmdADO.CreateParameter(Addresses, adVarWChar, adParamInput, Len(Addresses), Addresses)
    cmdADO.Parameters.Append prmText
    Set prmText = cmdADO.CreateParameter(Phones, adVarWChar, adParamInput, Len(Phones), Phones)
    cmdADO.Parameters.Append prmText
    cmdADO.Execute
    
    r.Open "SELECT * FROM LastId", c
	   
    If Not r.EOF Then
        Debug.Print "Inserted Contact ID:  & r(0)"
        SQL = "SELECT * FROM Contacts WHERE ContactID='" & r(0) & "'"
        r.Close
        
        r.Open SQL, c
        
        ' Workaround for ADOdb LONGNVARCHAR issue.
        For i = 0 To r.Fields.Count - 1
            If Not IsNull(r(i)) Then
                Debug.Print  "r(i).Name & :  & r(i) "
            Else
                Debug.Print  "r(i).Name & : NULL "
            End If
        Next
    End If
    
    r.Close
    Set r = Nothing
    GoTo AllDone
          
    ErrHandle:
        For Each e In c.Errors
            Debug.Print " ADODB Error:  & e.Description "
        Next
    AllDone:
        c.Close
        Set c = Nothing
        Debug.Print AddContact; Finished
End Sub

Example: update a contact's primary email address

Sub UpdateContactEmailAddress()
    Dim c As New ADODB.Connection
    Dim r As New ADODB.Recordset
    Dim e As ADODB.Error
    Dim cmdADO As New ADODB.Command
    Dim prmText As New ADODB.Parameter
    Dim rc As Long
    Dim i As Integer
    Dim SQL As String
    
    SQL =  "UPDATE Contacts SET EmailAddress='my_user@example.com' WHERE [Name]='example_customer'"
    
    ' On Error GoTo ErrHandle
    
    c.Open XeroDemo
    c.Execute SQL, rc
    
    Debug.Print  "Records affected:  & rc"
    
    r.Open  "SELECT LastID from LastId ", c

    If Not r.EOF Then
       Debug.Print  "UPDATE Contact ID:  & r(0)"
       SQL =  "SELECT [Name], EmailAddress FROM Contacts WHERE ContactID='" & r(0) &  "'"
       r.Close
       r.Open SQL, c
       
       For i = 0 To r.Fields.Count - 1
          If Not IsNull(r(i)) Then
             Debug.Print  "r(i).Name & :  & r(i) "
          Else
             Debug.Print  "r(i).Name & : NULL "
          End If
       Next
    End If
    
    r.Close
    Set r = Nothing
    GoTo AllDone
    
ErrHandle:
    For Each e In c.Errors
       Debug.Print  "ADODB Error:  & e.Description "
    Next
    
AllDone:
    c.Close
    Set c = Nothing
    Debug.Print  "UpdateContactEmailAddress; Finished "
End Sub

ContactGroups table

Supports INSERT and UPDATE statements.

SQL INSERT example

This SQL statement inserts a contact into a contact group.

INSERT INTO
   ContactGroups ( Name, Contacts ) 
VALUES
   (
      'New Contact Group' , '<Contacts><Contact>
                             <ContactID>af0091a9-82ef-4cac-9fd6-22c095ac6a58</ContactID>
                             <Name>Hamilton Smith Ltd</Name></Contact></Contacts>'
   )

If you create a contact group without specifying any contacts, Xero automatically adds all contacts.

SQL UPDATE example

This SQL statement updates a contact group's name.

UPDATE
   ContactGroups 
SET
   Name = 'Training 2' 
WHERE
   ContactGroupID = '91dbdc3f-86c5-4bfe-b227-5d1735945cea'

You can't currently update the Contacts column by using the Xero ODBC driver.

Note: SELECT queries

If you run:

SELECT * FROM ContactGroups 

the Contacts column is blank. This is a limitation in the Xero API. If you include a valid ContactGroupId:

SELECT * FROM ContactGroups WHERE ContactGroupId='91dbdc3f-86c5-4bfe-b227-5d1735945cea'

your result set will be similar to:

"ContactGroupID", "ModifiedAfter", "Name", "Status", "Contacts"
91dbdc3f-86c5-4bfe-b227-5d1735945cea, <Null>, Training, ACTIVE,
<Contacts><Contact><ContactID>af0091a9-82ef-4cac-9fd6-22c095ac6a58</ContactID>
<Name>Hamilton Smith Ltd</Name></Contact>
<Contact><ContactID>c523e12f-8b74-4d3a-bbd8-32d7a2f598b4</ContactID>
<Name>City Limousines</Name></Contact>
<Contact><ContactID>a852a44c-3d8f-4c4b-a628-3a2c2121b9b1</ContactID>
<Name>Bank West</Name></Contact>
<Contact><ContactID>847933f0-7c35-4e5b-b884-5f9df64c8e4b</ContactID>
<Name>Port &amp; Philip Freight</Name></Contact>
<Contact><ContactID>94a82e91-53da-4f87-a417-63d6a1607ced</ContactID>
<Name>Young Bros Transport</Name></Contact>
<Contact><ContactID>a871a956-05b5-4e2a-9419-7aeb478ca647</ContactID>
<Name>Ridgeway University</Name></Contact><Contact>
<ContactID>2dd82f49-e818-4dd0-955b-b637ccaa5597</ContactID>
<Name>City Agency</Name></Contact>
<Contact><ContactID>b2c1f980-96c9-45ff-a42b-dca141936c6c</ContactID>
<Name>Rex Media Group</Name></Contact></Contacts>

CreditNotes table

Supports INSERT and UPDATE statements.

SQL INSERT example

INSERT INTO
   CreditNotes ( [Date], ContactName, Type, CurrencyCode, CurrencyRate, LineItems ) 
values
   (
      '2024-02-07', 'PowerDirect', 'ACCRECCREDIT', 'GBP', 1, '<LineItems><LineItem>
       <Description>Monthly electricity</Description><UnitAmount>105.75</UnitAmount>
       <TaxType>RRINPUT</TaxType><TaxAmount>5.04</TaxAmount><LineAmount>105.75</LineAmount>
       <AccountCode>445</AccountCode><Quantity>1.0000</Quantity>
       <LineItemID>79393360-50bb-467f-a552-dd2be361201d</LineItemID>
       <AccountID>d50842c3-af67-4233-b8c9-df3180f5b7bd</AccountID>
       </LineItem></LineItems>'
   )

SQL UPDATE examples

This example changes the credited line to be half the amount credited.

UPDATE
   CreditNotes 
set
   SubTotal = 52.88, TotalTax = 2.52, Total = 52.88 + 2.52,
   LineItems = replace(replace(replace(LineItems, '<Quantity>1.0000</Quantity>', 
   '<Quantity>0.5</Quantity>'), '<LineAmount>105.75</LineAmount>', 
   '<LineAmount>52.88</LineAmount>'), '<TaxAmount>5.04</TaxAmount>',
   '<TaxAmount>2.52</TaxAmount>') 
WHERE
   CreditNoteId = '3812831b-b189-46e2-b8cc-3fee508aad83'

You can't delete a credit note but you can change its status to DELETED:

UPDATE
   CreditNotes 
SET
   Status = 'DELETED' 
WHERE
   CreditNoteID = '75e794a1-d09b-46c9-9ef7-bd749a5f8028'

Currencies table

Supports INSERT statements.

The following SQL statements both insert the speoified currency code.

SQL INSERT example

INSERT INTO Currencies ( Code ) VALUES ('USD')

SQL SELECT example that calls Xero API

SELECT
   * 
FROM
   api 
WHERE
   url = 'Currencies' 
   AND Method = 'PUT' 
   AND XMLRequestBody = '<Currency><Code>EUR</Code></Currency>'

Employees table

Supports: INSERT statements.

Example: adding employees

This VBA example checks worksheet cells for employees who don't have a Xero ID. Such employees are then added to Xero and the cells are updated with the relevant employee ID. The spreadsheet contents are:

Employee Id First Name Surname Xero ID
100 Andrew Fuller
101 Janet Leverling
102 Margaret Peacock

After running the VBA, the spreadsheet contents become:

Employee Id First Name Surname Xero ID
100 Andrew Fuller 5faffe3f-f4d0-49b4-8f97-2840206de7aa
101 Janet Leverling 49e761e6-b4f0-4919-beca-5aee296c385f
102 Margaret Peacock ea61757f-0c98-4113-9941-e3920befa614

Only new employees (spreadsheet entries without an Employee ID) are added on subsequent runs.

Sub SendEmployeeToXero()

    Dim c As New ADODB.Connection
    Dim r As New ADODB.Recordset
    Dim rPos As Long
    Dim rc As Long
    Dim SQL As String
    Dim rInserted As Long
    
    c.Open "XeroDemo"
    rPos = 2
    
    ' Walk down the sheet collecting the rows data
    rInserted = 0
    Do While Not IsNull(Cells(rPos, 2)) And Cells(rPos, 2) <> ""
    
        If Not IsNull(Cells(rPos, 2)) And Cells(rPos, 2) <> "" _
            And (IsNull(Cells(rPos, 4)) Or Cells(rPos, 4) = "") Then

            SQL = "INSERT INTO Employees ( FirstName, LastName ) VALUES " & _
                "('" & Replace(Cells(rPos, 2), "'", "''") & "', '" & _
                Replace(Cells(rPos, 3), "'", "''") & "')"
            c.Execute SQL, rc
            
            If rc <> 0 Then
                r.Open "select LastID from LastId", c
                If Not IsNull(r(0)) Then
                    Cells(rPos, 4) = r(0)
                End If
                r.Close
                rInserted = rInserted + 1
            End If
        End If
        
        rPos = rPos + 1
    Loop
    
    MsgBox "Insert rows : " & rInserted
    
    c.Close
    
    Set r = Nothing
    Set c = Nothing
End Sub

ExpenseClaims table

Supports INSERT statements.

Although ExpenseClaims was deprecated in 2018 by Xero, the endpoint is still available.

SQL INSERT example

INSERT INTO
   ExpenseClaims ( UserId, Receipts ) 
values
   (
      '73f0cf2f-ede4-49ea-b93e-6723d9dfca32', '<Receipts>
                                                 <ReceiptID>
                                                   7b67f5d8-bee4-42a2-a184-6742a2f79df1
                                                 </ReceiptID>
                                               </Receipts>' 
   )

Note Not all Xero accounts have access to ExpenseClaims.

Invoices tables

Supports INSERT and UPDATE statements.

Example: adding a new invoice

This VBA code sample adds a new invoice. The invoice line items are added into the LineItems in the XML format.

Sub CreateBasicInvoice()
    Dim c As New ADODB.Connection
    Dim r As New ADODB.Recordset
    Dim e As ADODB.Error
    Dim i As Integer
    
    Dim SQL As String
    
    ' This is a basic invoice insert. Notes:
    ' Date needs to be enclosed in [] as it is a reserved word
    ' Date format must be yyyy-mm-dd
    ' InvoiceNumber must be unique

    SQL = "INSERT INTO Invoices (InvoiceNumber, Type, ContactID, ContactName, [Date], LineItems ) " & _
        "VALUES ( 'INV-0346', 'ACCREC', '9ce626d2-14ea-463c-9fff-6785ab5f9bfb', " & _
        "'Boom FM', '2024-01-31', '<LineItems><LineItem>" & _
        "<Description>Retainer for consulting work</Description><UnitAmount>500.00</UnitAmount>" & _
        "<AccountCode>200</AccountCode><Quantity>3</Quantity>" & _
        "<AccountID>c563b607-fb0e-4d06-9ddb-76fdeef20ae3</AccountID></LineItem></LineItems>')"
    
    c.Open XeroDemo
    
    On Error GoTo ErrHandle
    
    ' Insert the invoice
    c.Execute SQL
    
    ' If the INSERT succeeds, fetch all the columns.
    r.Open "SELECT * FROM LastId", c
    If Not r.EOF Then
       Debug.Print "Inserted Invoice ID:  & r(0)"
       SQL = "SELECT * FROM Invoices WHERE InvoiceID='" & r(0) & "'"

       r.Close
       r.Open SQL, c
       
       ' Workaround for ADOdb LONGNVARCHAR issue.
       For i = 0 To r.Fields.Count - 1
           If Not IsNull(r(i)) Then
               Debug.Print "r(i).Name & :  & r(i)"
           Else
               Debug.Print "r(i).Name & : NULL"
           End If
       Next
    End If
    
    r.Close
    Set r = Nothing
    GoTo AllDone
    
ErrHandle:
    For Each e In c.Errors
        Debug.Print "ADODB Error:  & e.Description"
    Next
    
AllDone:
    c.Close
    Set c = Nothing
    Debug.Print "CreateBasicInvoice Finished."
End Sub

Example: adding an invoice line to the end of an invoice

Sub AddAdditionalLineToInvoice()
          
    Dim c As New ADODB.Connection
    Dim r As New ADODB.Recordset
    Dim e As ADODB.Error
    Dim cmdADO As New ADODB.Command
    Dim prmText As New ADODB.Parameter
    Dim li As String
    Dim SQL As String
    Dim AddItem As String
    
    ' Add a line to an existing invoice.

    AddItem = "<LineItem><ItemCode>BOOK</ItemCode><Description>Fish out of Water: " & _
        "Finding Your Brand</Description><UnitAmount>19.95</UnitAmount><Item>" & _
        "<ItemID>5b27339e-ecc8-40b6-ace3-0f25bdf5fd63</ItemID><Name>Fish out of Water:" & _
        "Finding Your Brand</Name><Code>BOOK</Code></Item><Quantity>2</Quantity>" & _
        "<AccountID>c563b607-fb0e-4d06-9ddb-76fdeef20ae3</AccountID></LineItem>"
    c.Open XeroDemo
    
    SQL = "SELECT LineItems FROM Invoices WHERE InvoiceNumber='INV-0346'"
    
    r.Open SQL, c
    If r.EOF Then
        Debug.Print "Invoice; missing"
        r.Close
        GoTo AllDone
    Else
        li = r(0)
        r.Close
        
        SQL = "UPDATE Invoices SET LineItems=? WHERE InvoiceNumber='INV-0346'"
        
        ' This puts the new line at the end of the items list.
        li = Replace(li, "</LineItems>, AddItem & </LineItems>")
        
        On Error GoTo ErrHandle
        
        cmdADO.ActiveConnection = c
        cmdADO.CommandText = SQL
        Set prmText = cmdADO.CreateParameter(LineItems, adVarWChar, adParamInput, Len(li), li)
        cmdADO.Parameters.Append prmText
        cmdADO.Execute
    
    End If
    
    GoTo AllDone
    
ErrHandle:
    For Each e In c.Errors
        Debug.Print "ADODB Error:  & e.Description"
    Next
    
AllDone:
    c.Close
    Set c = Nothing
    Debug.Print "AddAdditionalLineToInvoice Finished."
End Sub

Items table

Supports INSERT, UPDATE, and DELETE statements.

This table has one row per item.

Example: copying items

This VBA code sample copies items from a Microsoft Access table to Xero. The table structure is:

LocalProducts
Code Name Description Price
0017-99-9999-99-31-12-01 Easysoft ODBC-Oracle Driver 1 Machine 999.99
0020-99-9999-99-31-12-01 Easysoft dbExpress-ODBC Gateway 1 Machine 225
0022-99-9999-99-31-12-01 Easysoft ODBC-Interbase Driver 1 Machine 750
0026-99-9999-99-31-12-01 Easysoft ODBC-JDBC Gateway 1 Machine 6498
0028-99-9999-99-31-12-01 Easysoft ODBC-Sybase Driver 1 Machine 1099
0039-99-9999-99-31-12-01 Easysoft ODBC-Firebird Driver 1 Machine 450
0044-99-9999-99-31-12-01 Easysoft ODBC-LINC Developer Driver 1 Machine 3199

The example inserts all items as if they are untracked and not purchased from a third party.

Sub LocalItemsIntoXero()
    Dim db As DAO.Database
    Dim rs As DAO.Recordset
    
    Dim c As New ADODB.Connection
    Dim r As New ADODB.Recordset
    Dim e As ADODB.Error
    Dim SQL As String
    
    ' Insert Access table data (LocalProducts) as Xero items
    Set db = CurrentDb
    Set rs = db.OpenRecordset(LocalProducts)
    
    c.Open XeroDemo
    
    Do While Not rs.EOF
    
        ' Check that the item does not already exist
        r.Open "SELECT COUNT(*) FROM Items WHERE Code='" & rs(Code) & "'", c
        If r(0) = 0 Then
            r.Close

            SQL = "insert into Items ( Code, Name, Description, SalesUnitPrice, IsPurchased, " & _
                "SalesTaxType, SalesAccountCode, IsTrackedAsInventory ) values ( " & _
                "'" & rs("Code") & "', '" & rs("Name") & "', '" & rs("Description") & "', " & _
                rs("Price") & ", 0, 'OUTPUT2', '200', 0 )"
            
            c.Execute SQL
            r.Open "SELECT * FROM LastId", c
            If Not r.EOF Then
                Debug.Print Item ' & rs(Name) & ' insert as Item Id:  & r(0)
            End If
        End If
        r.Close
        rs.MoveNext
    Loop
    
    rs.Close
    db.Close
    
    Set rs = Nothing
    Set db = Nothing
    
    c.Close
    Set c = Nothing
         
End Sub

SQL UPDATE example

UPDATE Items SET SalesUnitPrice=1000 WHERE ItemID='2033478a-79d4-409d-bfee-c3cc23c15884'

SQL DELETE example

DELETE FROM Items WHERE ItemID='2033478a-79d4-409d-bfee-c3cc23c15884'

LinkedTransactions table

Supports INSERT, UPDATE, and DELETE statements.

SQL INSERT example

INSERT INTO
   LinkedTransactions ( Type, SourceTransactionId, SourceLineItemId,
                        SourceTransactionTypeCode, ContactId ) 
VALUES
   (
      'BILLABLEEXPENSE', 'b1b4f969-a6e3-478c-b0e9-e232d6d81724',
      '3e50e9ff-49da-4fc3-8f80-99f97248fef1', 'ACCPAY',
      '9ce626d2-14ea-463c-9fff-6785ab5f9bfb'
   )

SQL UPDATE example

UPDATE LinkedTransactions SET ContactId='6f5b2926-224f-4ac5-9632-c0cd1e5551c5'
WHERE LinkedTransactionId='70ce9c5d-790a-41f8-9695-079cd143de82'

SQL DELETE example

DELETE FROM LinkedTransactions
WHERE LinkedTransactionId='25977bea-2ed6-404c-b770-6c5e00176ac7'

ManualJournals table

Supports INSERT and UPDATE statements.

SQL INSERT example

INSERT INTO
   ManualJournals ( [Date], Narration, JournalLines ) 
VALUES
   (
      '2024-02-06', 'This is a test', '<JournalLines><JournalLine>
<Description>Coded incorrectly Computer Equipment should be Office Equipment</Description>
<TaxType>NONE</TaxType><TaxAmount>0.00</TaxAmount><LineAmount>2569.00</LineAmount>
<AccountCode>710</AccountCode><AccountID>a4602fb6-2e9d-4064-b318-c409032692ba
</AccountID></JournalLine><JournalLine>
<Description>Coded incorrectly Office Equipment should be Computer Equipment</Description>
<TaxType>NONE</TaxType><TaxAmount>0.00</TaxAmount><LineAmount>-2569.00</LineAmount>
<AccountCode>720</AccountCode><AccountID>8d1ae68b-1251-4b44-9d7a-639b3976935c</AccountID>
</JournalLine></JournalLines>'
   )

Date is a reserved word, so enclose this value with square brackets.

SQL UPDATE example

UPDATE
   ManualJournals 
SET
   Narration = 'Narration Updated' 
WHERE
   ManualJournalID = '1811d1fa-fa76-41a2-a0f5-1fa982017591'

PurchaseOrders table

Supports INSERT and UPDATE statements.

SQL INSERT example

INSERT INTO
   PurchaseOrders ( ContactID, [Date], Reference, CurrencyCode, CurrencyRate, LineItems) 
VALUES
   (
      '699f0091-b127-4796-9f15-41a2f42abeb2', '2023-02-07', 'POTest 1', 'USD',
       0.725361, '<LineItems><LineItem><ItemCode>GB9-White</ItemCode>
                  <Description>Golf balls - white - 9-pack</Description>
                  <UnitAmount>25.0000</UnitAmount><TaxType>INPUT2</TaxType>
                  <TaxAmount>41.67</TaxAmount><LineAmount>250.00</LineAmount>
                  <AccountCode>300</AccountCode><Quantity>10.0000</Quantity>
                  <LineItemID>6a2045bd-c21c-454b-8e99-540ec96748f6</LineItemID>
                  </LineItem></LineItems>' 
   )

SQL UPDATE example

UPDATE
   PurchaseOrders 
SET
   LineItems = REPLACE(LineItems, '<Quantity>10.0000</Quantity>', '<Quantity>2</Quantity>') 
WHERE
   PurchaseOrderId = 'd646f075-6bec-47a9-a59c-fe6e97c4f849'

Quotes tables

Supports INSERT and UPDATE statements.

SQL INSERT example

INSERT INTO
   Quotes ( [Date], ExpiryDue, CurrencyCode, CurrencyRate, ContactId, LineItems ) 
values
   (
      '2024-02-01', '2024-03-01', 'USD', 1.3, '9ce626d2-14ea-463c-9fff-6785ab5f9bfb',
      '<LineItems><LineItem><LineItemID>2ce92ec0-a1bd-4782-8cdd-5edbfe498f90</LineItemID>
       <AccountCode>200</AccountCode><Description>Unlimited Clients LAN - 1 Server
       LAN</Description><UnitAmount>4800.0000</UnitAmount><DiscountRate>0.42</DiscountRate>
       <DiscountAmount>20.00</DiscountAmount><LineAmount>4780.00</LineAmount>
       <ItemCode>0017-99-9999-99-37-12-01</ItemCode><Quantity>1.0000</Quantity>
       <TaxAmount>0.00</TaxAmount><TaxType>NONE</TaxType></LineItem><LineItem>
       <LineItemID>51ec56a4-9663-475e-a6b9-1a93b484b0ef</LineItemID><AccountCode>200
       </AccountCode><Description>Unlimited Machines on LAN</Description>
       <UnitAmount>12499.0000</UnitAmount><DiscountRate>0.16</DiscountRate>
       <DiscountAmount>20.00</DiscountAmount><LineAmount>12479.00</LineAmount>
       <ItemCode>0026-99-9999-99-32-12-01</ItemCode><Quantity>1.0000</Quantity>
       <TaxAmount>0.00</TaxAmount><TaxType>NONE</TaxType></LineItem></LineItems>'
   )

The previous example inserts a two-line quote for a customer referenced by the CustomerId only. If you send a quote that uses an existing quote number, Xero will generate a new quote number for the quote.

Receipts table

Supports INSERT and UPDATE statements.

Although Receipts was deprecated in 2018 by Xero, the endpoint is still available.

SQL UPDATE example

UPDATE
   Receipts 
SET
   LineItems = '<LineItems><LineItem><Description>Drinks &amp; snacks for staff meeting</Description>
                <UnitAmount>35.20</UnitAmount><TaxType>INPUT2</TaxType>
                <TaxAmount>5.87</TaxAmount><LineAmount>35.20</LineAmount>
                <AccountCode>429</AccountCode><Quantity>1.0000</Quantity>
                <AccountID>f96c9458-d724-47bf-8f74-a9d5726465ce</AccountID>
                <DiscountEnteredAsPercent>true</DiscountEnteredAsPercent></LineItem></LineItems>' 
where
   ReceiptId = '7b67f5d8-bee4-42a2-a184-6742a2f79df1'

TaxRates table

Supports INSERT and UPDATE statements.

SQL UPDATE example

UPDATE
   TaxRates 
SET
   TaxComponents = replace(TaxComponents, '<Rate>17.5000</Rate>', '<Rate>1.7500</Rate>') 
WHERE
   Name = '17.5% (VAT on Income)'

TrackingCategories table

Supports: INSERT and UPDATE statements.

SQL INSERT example

INSERT INTO TrackingCategories ( Name ) VALUES ('Van Deliveries')

To create a new option for these tracking categories, get the TrackingCategoryID:

SELECT LastID from LastId

then use the ID in a query similar to:

SELECT
   * 
FROM
   api 
WHERE
   url = 'TrackingCategories/201a3021-77de-495d-a233-1faaa40e2f64/Options' 
   AND Method = 'PUT' 
   AND XMLRequestBody = '<Name>TrackOpt 1</Name>'

which returns::

"URL", "Method", "Code", "XMLRequestBody", "Response", "XMLResponseBody"
"TrackingCategories/201a3021-77de-495d-a233-1faaa40e2f64/Options", "PUT", 200,
"<Name>TrackOpt 1</Name>", "OK",
"<Response xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Id>57a11291-f2b2-49fe-84f2-b8370af16278</Id>
<Status>OK</Status>
<ProviderName>Easysoft</ProviderName>
<DateTimeUTC>2024-02-07T14:13:28.6181931Z</DateTimeUTC>
</Response>"

to fetch the new tracking category option:

SELECT
   * 
FROM
   api 
WHERE
   url = 'TrackingCategories/201a3021-77de-495d-a233-1faaa40e2f64/Options' 
   AND Method = 'PUT' 
   AND JSONRequestBody = '{ "Name": "New Track 5" }'

Formatting XML

This VBA function formats XML to improve readability. Use the code with the other VBA samples in this guide.

Add a reference to the "Microsoft XML, v6.0" library before running the code.

Public Function PrettyPrintXML(ByRef xml As String, ByRef RemoveXMLFirstLine As Boolean) As String
    Dim Reader As New SAXXMLReader60
    Dim Writer As New MXXMLWriter60
    Dim blHasError As Boolean
    Dim XMLData As String
    
    On Error GoTo ErrHandle
    
    XMLData = xml
    
    Writer.Indent = True
    Writer.standalone = False
    Writer.omitXMLDeclaration = False
    Writer.Encoding = "utf-8"
    
    Set Reader.contentHandler = Writer
    Set Reader.dtdHandler = Writer
    Set Reader.ErrorHandler = Writer
    
    Call Reader.parse(XMLData)
    
    GoTo Finished
        ErrHandle:
    blHasError = True
        Finished:
    If blHasError Then
        PrettyPrintXML = "ERROR: " & vbCrLf & vbCrLf & XMLData
    Else
        XMLData = Writer.output
    If RemoveXMLFirstLine Then XMLData = Right(XMLData, Len(XMLData) - InStr(1, XMLData, ">", vbBinaryCompare) - 2)
        PrettyPrintXML = XMLData
    End If
End Function

Using the Xero ODBC driver with SQL Server

To run INSERT, UPDATE, or DELETE statements with a Xero ODBC driver-based linked server, set both these linked server properties to True: Server Options > RPC and Server Options > RPC Out. If you are only using the API table, leave these properties set to False.

HTTP 400 Bad Request

If you execute multiple INSERT, UPDATE, or DELETE statements in quick succession, the Xero API may return the 400 status and "Bad Request <no further details>." This seems to be a limitation of the Xero API. We worked around this by using a 0.5 second pause between statements.

If you get this error, contact the Easysoft Support team ().

SQL format and function support

The Easysoft ODBC-Xero driver supports the SQL-92 syntax:

Supported string functions

Function Description
ASCII(string_exp) Returns the ASCII code value of the leftmost character of string_exp as an integer.
BIT_LENGTH(string_exp) Returns the length in bits of the string expression.
CHAR(code) Returns the character that has the ASCII code value specified by code. The value of code should be between 0 and 255, otherwise the return value is data source-dependent.
CHAR_LENGTH(string_exp) Returns the length in characters of the string expression, if the string expression is of a character data type, otherwise returns the length in bytes of the string expression (the smallest integer not less than the number of bits divided by 8). This is the same function as CHARACTER_LENGTH.
CHARACTER_LENGTH Refer to CHAR_LENGTH
CONCAT(string_exp1, string_exp2) Returns a character string that is the result of concatenating string_exp2 to string_exp1. The resulting string is DBMS-dependent.
DIFFERENCE(string_exp1, string_exp2) Returns an integer value that indicates the difference between the values returned by the SOUNDEX function for string_exp1 and string_exp2.
INSERT(string_exp1, start, length, string_exp2) Returns a character string where length characters have been deleted from string_exp1 beginning at start and where string_exp2 has been inserted into string_exp, beginning at start.
LCASE(string_exp) Returns a string equal to that in string_exp with all uppercase characters converted to lowercase.
LEFT(string_exp, count) Returns the leftmost count characters of string_exp.
LENGTH(string_exp) Returns the number of characters in string_exp, excluding trailing blanks.
LOCATE(string_exp1, string_exp2[,start]) Returns the starting position of the first occurrence of string_exp1 within string_exp2. The search for the first occurrence of string_exp1 begins with the first character position in string_exp2 unless the optional argument, start, is specified. If start is specified, the search begins with the character position indicated by the value of start. The first character position in string_exp2 is indicated by the value1. If string_exp1 is not found within string_exp2, the value 0 is returned.
LTRIM(string_exp) Returns the characters of string_exp, with leading blanks removed.
OCTET_LENGTH(string_exp) Returns the length in bytes of the string expression. The result is the smallest integer not less than the number of bits divided by 8.
POSITION(char_exp IN char_exp) Returns the position of the first character expression in the second character expression. The result is an exact numeric with an implementation-defined precision and a scale of 0.
REPEAT(string_exp, count) Returns a character string composed of string_exp repeated count times.
REPLACE(string_exp1, string_exp2, string_exp3) Search string_exp1 for occurrences of string_exp2 and replace with string_exp3.
RIGHT(string_exp, count) Returns the rightmost count characters of string_exp.
RTRIM(string_exp) Returns the characters of string_exp with trailing blanks removed.
SOUNDEX(string_exp) Returns a data source-dependent character string representing the sound of the words in string_exp.
SPACE(count) Returns a character string consisting of count spaces.
SUBSTRING(string_exp, start, length) Returns a character string that is derived from string_exp beginning at the character position specified by start for length characters.
TRIM This is an SQL-92 version of the ODBC LTRIM and RTRIM functions.
UCASE(string_exp) Returns a string equal to that in string_exp with all lowercase characters converted to uppercase.

Supported numeric functions

Function Description
ABS(numeric_exp) Returns the absolute value of numeric_exp.
ACOS(float_exp) Returns the arccosine of float_exp as an angle, expressed in radians.
ASIN(float_exp) Returns the arcsine of float_exp as an angle, expressed in radians.
ATAN(float_exp) Returns the arctangent of float_exp as an angle, expressed in radians.
ATAN2(float_exp1, float_exp2) Returns the arctangent of the x and y coordinates, specified by float_exp1 and float_exp2 respectively, as an angle expressed in radians.
CEILING(numeric_exp) Returns the smallest integer greater than or equal to numeric_exp.
COS(float_exp) Returns the cosine of float_exp where float_exp is an angle expressed in radians.
COT(float_exp) Returns the cotangent of float_exp where float_exp is an angle expressed in radians.
DEGREES(numeric_exp) Returns the number of degrees converted from numeric_exp radians.
EXP(float_exp) Returns the exponential value of float_exp.
FLOOR(numeric_exp) Returns the largest integer less than or equal to numeric_exp.
LOG(float_exp) Returns the natural logarithm of float_exp.
LOG10(float_exp) Returns the base 10 logarithm of float_exp.
MOD(integer_exp1, integer_exp2) Returns the remainder (modulus) of integer_exp1 divided by integer_exp2.
PI() Returns the constant value of pi as a floating point value.
POWER(numeric_exp, integer_exp) Returns the value of numeric_exp to the power of integer_exp.
RADIANS(numeric_exp) Returns the number of radians converted from numeric_exp degrees.
RAND([integer_exp]) Returns a random floating point value using integer_exp as the optional seed value.
ROUND(numeric_exp, integer_exp) Returns numeric_exp rounded to integer_exp places right of the decimal point. If integer_exp is negative, numeric_exp is rounded to integer_exp places to the left of the decimal point.
SIGN(numeric_exp) Returns an indicator of the sign of numeric_exp. If numeric_exp is less than zero, -1 is returned. If numeric_exp equals zero, 0 is returned. If numeric_exp is greater than zero, 1 is returned.
SIN(float_exp) Returns the sine of float_exp, where float_exp is an angle expressed in radians.
SQRT(float_exp) Returns the square root of float_exp.
TAN(float_exp) Returns the tangent of float_exp where float_exp is an angle expressed in radians.
TRUNCATE(numeric_exp, integer_exp) Returns numeric_exp truncated to integer_exp places right of the decimal point. If integer_exp is negative, numeric_exp is truncated to integer_exp places to the left of the decimal point.

Supported date and time functions

Function Description
CURRENT_DATE() Returns the current date.
CURRENT_TIME[(time-precision)] Returns the current local time. The time-precision argument determines the seconds precision of the returned value.
CURRENT_TIMESTAMP[(timestamp-precision)] Returns the current local date and local time as a timestamp value. The timestamp-precision argument determines the seconds precision of the returned timestamp.
CURDATE() Returns the current date.
CURTIME() Returns the current local time.
DAYNAME(date_exp) Returns a character string containing the data source-specific name of the day for the day portion of date_exp.
DAYOFMONTH(date_exp) Returns the day of the month based on the month field in date_exp as an integer value in the range 1-31.
DAYOFWEEK(date_exp) Returns the day of the week based on the week field in date_exp as an integer value in the range of 1–7 where 1 represents Sunday.
DAYOFYEAR(date_exp) Returns the day of the year based on the year field in date_exp as an integer value in the range of 1–366.
EXTRACT(extract-field FROM extract-sourc) Returns the extract-field portion of the extract-source. The extract-source argument is a datetime or interval expression. The extract-field argument can be one of the YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND keywords. The precision of the returned value is implementation-defined. The scale is 0 unless SECOND is specified, in which case the scale is not less that the fractional seconds precision of the extract-source field.
HOUR(time_exp) Returns the hour based on the hour field in time_exp as an integer value in the range of 0–23.
MINUTE(time_exp) Returns the minute based on the minute field in time_exp as an integer value in the range of 0–59.
MONTH(date_exp) Returns the month based on the month field in date_exp as an integer value in the range 1–12.
MONTHNAME(date_exp) Returns a character string containing the data source-specific name of the month for the month portion of date_exp.
NOW() Returns the current date and time as a timestamp value.
QUARTER(date_exp) Returns the quarter in date_exp as an integer value in the range of 1–4.
SECOND(time_exp) Returns the second based on the second field in time_exp as an integer value in the range of 0–59.
TIMESTAMPADD(interval, integer_exp, timestamp_exp) Returns the timestamp calculated by adding integer_exp intervals of type interval to timestamp_exp. Valid values of interval are the following keywords: SQL_TSI_FRAC_SECOND, SQL_TSI_SECOND, SQL_TSI_MINUTE, SQL_TSI_HOUR, SQL_TSI_DAY, SQL_TSI_WEEK, SQL_TSI_MONTH, SQL_TSI_QUARTER, or SQL_TSI_YEAR where fractional seconds are expressed in billionths of a second. If time_stamp is a time value and interval specifies days, weeks, months, quarters, or years, the date portion of timestamp_exp is set to the current date before calculating the resulting timestamp. If timestamp_exp is a date value and interval specifies fractional seconds, seconds, minutes, or hours, the time portion of timestamp_exp is set to 0 before calculating the resulting timestamp. An application determines which intervals a data source supports by calling SQLGetInfo with the SQL_TIMEDATE_ADD_INTERVALS option.
TIMESTAMPDIFF(interval, timestamp_exp1, timestamp_exp2) Returns the integer number of intervals of type interval by which timestamp_exp2 is greater than timestamp_exp1. The keywords SQL_TSI_FRAC_SECOND, SQL_TSI_SECOND, SQL_TSI_MINUTE, SQL_TSI_HOUR, SQL_TSI_DAY, SQL_TSI_WEEK, SQL_TSI_MONTH, SQL_TSI_QUARTER, or SQL_TSI_YEAR are valid values of interval, where fractional seconds are expressed in billionths of a second. If either timestamp expression is a time value and interval specifies days, weeks, months, quarters, or years, the date portion of that timestamp is set to the current date before calculating the difference between timestamps. If either timestamp expression is a date value and interval specifies fractional seconds, seconds, minutes, or hours, the time portion of that timestamp is set to 0 before calculating the difference between timestamps. An application determines which intervals a data source supports by calling SQLGetInfo with the SQL_TIMEDATE_DIFF_INTERVALS option.
WEEK(date_exp) Returns the week of the year based on the week field in date_exp as an integer value in the range of 1–53.
YEAR(date_exp) Returns the year based on the year field in date_exp as an integer value. The range is data source-dependent.