1

I am attempting to access the Appointments-Plus.com API via VBA code and am consistently being told I'm giving it an invalid site-id/key. I'm using Access from the Office365 version.

This is the relevant documentation for this API call.

When I use Postman to test out the API, I am able to successfully connect and I get data back. The URL Postman puts together is this:

https://ws.appointment-plus.com/Locations/GetLocations?Authorization:Basic=<site-ID>:<Key>&response_type=xml

My VBA code is this:

Public Sub RESTtestBigURL()

  Dim responseType As String
  responseType = "response_type=json"

  Dim restRequest As WinHttp.WinHttpRequest
  Set restRequest = New WinHttp.WinHttpRequest

  Dim restResult As String

  With restRequest
    .Open "POST", "https://ws.appointment-plus.com/Locations/GetLocations?Authorization:Basic=<site-ID>:<Key>&response_type=xml", False
    .Send
    .WaitForResponse
    Debug.Print ".ResponseText: " & .ResponseText
    Debug.Print ".Status: " & .Status
    Debug.Print ".StatusText: " & .StatusText
    Debug.Print ".ResponseBody: " & .ResponseBody
  End With

End Sub

I know that the first question is "are you sure you've got the <site-ID> and <key> correct???" Yes - I've copy/pasted the entire URL from Postman into my VBA code, and I've had another couple of pairs of eyeballs review it to confirm that they're still the same.

When I run that code, I get:

.ResponseText: <?xml version="1.0" encoding="utf-8" ?>
<APResponse>
  <resource>customers</resource>
  <action>getcustomers</action>
  <request></request>
  <result>fail</result>
  <count>0</count>
  <errors>
    <error><![CDATA[Web Services authentication failed: invalid Site ID or API Key]]></error>
  </errors>
</APResponse>

I've tried several other methods of accessing the API, all of which are giving me the same "Invalid ID/Key" error:

Public Sub SecondRESTtestMSXML()

  Dim restRequest As MSXML2.XMLHTTP60
  Set restRequest = New MSXML2.XMLHTTP60

  With restRequest

    .Open "GET", URL & REQUEST_GET_LOCATIONS, True
    .SetRequestHeader "Authorization", "Basic" & SITE_ID & ":" & API_KEY
    .SetRequestHeader "response_type", "xml"
    .SetRequestHeader "Accept-Encoding", "application/xml"
    .Send "{""response_type"":""JSON""&""location"":""582""}"
    While .ReadyState <> 4
      DoEvents
    Wend
    Debug.Print ".ResponseText: " & .ResponseText
    Debug.Print ".Status: " & .Status
    Debug.Print ".StatusText: " & .StatusText
    Debug.Print ".ResponseBody: " & .ResponseBody

  End With

End Sub

There is a suggestion that this is a duplicate of another question that was resolved by Base64-encoding. However, this method, while it wasn't explicit, shows that I have attempted that, too. I've added the Base64Encode function code that is called from here.

Public Sub RESTtest()

  Dim restRequest As WinHttp.WinHttpRequest
  Set restRequest = New WinHttp.WinHttpRequest

  Dim restResult As String

  With restRequest
    .Open "POST", URL & REQUEST_GET_LOCATIONS, True
    .SetRequestHeader "Authorization", "Basic " & SITE_ID & ":" & Base64Encode(API_KEY)
' Note call to Base64Encode() on this line ---------------- ----- ^^^^^^^^^^^^
    .Option(WinHttpRequestOption_EnableRedirects) = False
    .Send "{""response_type"":""JSON""}"
    .WaitForResponse
    Debug.Print ".ResponseText: " & .ResponseText
    Debug.Print ".Status: " & .Status
    Debug.Print ".StatusText: " & .StatusText
    Debug.Print ".ResponseBody: " & .ResponseBody
  End With

End Sub

Public Function Base64Encode(ByVal inputText As String) As String
  Dim xmlDoc As Object
  Dim docNode As Variant
  Set xmlDoc = CreateObject("Msxml2.DOMDocument.3.0")
  Set docNode = xmlDoc.createElement("base64")
  docNode.DataType = "bin.base64"
  docNode.nodeTypedValue = Stream_StringToBinary(inputText)
  Base64Encode = docNode.Text
  Set docNode = Nothing
  Set xmlDoc = Nothing
End Function

Notes:

  • URL, REQUEST_GET_LOCATIONS, SITE_ID, and API_KEY are constants declared globally in this module for testing purposes. They, too, have all been copy/pasta'd and reviewed by several people for typos.
  • You may note that there are requests for responses in both XML and JSON - they're both giving me the same response.
  • I do have a support ticket open with Appt Plus, but I'm hoping I might get a faster response here.

Are there any obvious errors that anyone sees in this code? Are there any suggestions for other methods to attempt to call the API and get results? I've had a suggestion to write a DLL in C# and call that, however, I don't have the time to learn enough C# to make that happen, so switching languages isn't really an option here.

Additional notes:

I tried this using curl in a Powershell session, and it gives me the same result:

PS H:\> curl -method Post -uri "https://ws.appointment-plus.com/Locations/GetLocations?Authorization:Basic=<ID>:<key>&response_type=json"

The result:

StatusCode        : 200
StatusDescription : OK
Content           : {"resource":"locations",
                     "action":"getlocations",
                     "request":"",
                     "result":"fail",
                     "count":"0"
                    ,"errors":[
                        "Web Services authentication failed: invalid Site ID or API ...
RawContent        : HTTP/1.1 200 OK
                    Pragma: no-cache
                    Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
                    Date: Mon, 26 Aug 2019 17:28:41 GMT
                    Expires: Thu, 19 Nov 1981 08:52:00 GMT
                    Set-Cooki...
Forms             : {}
Headers           : {[Pragma, no-cache], [Cache-Control, no-store, no-cache, must-revalidate, post-check=0,
                    pre-check=0], [Date, Mon, 26 Aug 2019 17:28:41 GMT], [Expires, Thu, 19 Nov 1981 08:52:00 GMT]...}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 207
FreeMan
  • 5,660
  • 1
  • 27
  • 53
  • I think it might be how the credentials are being sent. Basic Auth should the the Base64 encoding of the username + ":" + password. See -- > https://en.wikipedia.org/wiki/Basic_access_authentication – Ryan Wildry Aug 26 '19 at 18:30
  • Possible duplicate of [How to pass API key in VBA for Get and Post Request?](https://stackoverflow.com/questions/50185113/how-to-pass-api-key-in-vba-for-get-and-post-request) – June7 Aug 26 '19 at 18:32
  • As noted in my post, I do have an attempt using Base64 encoding: `.SetRequestHeader "Authorization", "Basic " & SITE_ID & ":" & Base64Encode(API_KEY)` and I still get those results. I've updated the post to include the `Base64Encode` function. – FreeMan Aug 26 '19 at 18:39
  • @Freeman, you need to encode the UserName *and* Password in Base64. You are only doing the password. E.g. Base64Encode(UserName + ":" + Password). Have a look at the link I had in my earlier comment. – Ryan Wildry Aug 26 '19 at 19:44
  • @RyanWildry I've tried both `.Open "POST", URL & REQUEST_GET_CUSTOMERS & "?Authorization:Basic=" & Base64Encode(SITE_ID) & ":" & Base64Encode(API_KEY) & "&" & responseType, False` and `.Open "POST", URL & REQUEST_GET_CUSTOMERS & "?Authorization:Basic=" & Base64Encode(SITE_ID & ":" & API_KEY) & "&" & responseType, False` (encoding the uid & pwd separately in the 1st instance, together in the 2nd) and both give me the same "invalid ID/Key" response. – FreeMan Aug 26 '19 at 19:53
  • Additionally, is this Base64 Encoding something that Postman is doing in the background without telling me? I see no references to it there. (Yes, my HTTP knowledge is _very_ limited.) – FreeMan Aug 26 '19 at 19:54
  • 1
    Yes, Postman does this automatically when you select the authorization type/method. This encoded value goes into the `Authorization` header, not the request. E.g. `.SetRequestHeader "Authorization", "Basic " & Base64Encode(SITE_ID & ":" & API_KEY)` – Ryan Wildry Aug 26 '19 at 19:59
  • I'm following you now @RyanWildry, thank you. Unfortunately, I've run into a different issue - `Base64Encode(SITE_ID & ":" & API_KEY) is returning a string containing a `CrLf` (it's split over 2 lines when I print it in the `Immediate Window`, and I'm getting `RTE 80070057 - The parameter is incorrect` when I attempt to `.SetRequestHeader` with the result of that encoding. – FreeMan Aug 26 '19 at 20:15
  • 1
    Remove the line breaks. Have a look here. https://stackoverflow.com/a/41838865/4839827 – Ryan Wildry Aug 26 '19 at 20:20
  • @RyanWildry I'm getting a time out now, but I'm pretty sure I can work my way around that. TYVM for your help. I believe this is different enough from the other answer because that one didn't specify encoding both the ID & PWD and doesn't deal with removing the potential `vbLf` from the resulting encoded string. Therefore, if you desire to get yourself some magical internet points, redeemable for unicorns and leprechan gold, please feel free to write all this up as an answer. – FreeMan Aug 26 '19 at 20:30
  • @FreeMan great to hear, hope it helps. I added an answer. – Ryan Wildry Aug 26 '19 at 20:40
  • 1
    Just a note, you may want to have a look at https://github.com/VBA-tools/VBA-Web -- it greatly simplifies working with REST API from VBA. See https://stackoverflow.com/a/15801781/3820271 – Andre Aug 27 '19 at 12:54
  • Thank you @Andre. I'd already downloaded that and am beginning to look at it. – FreeMan Aug 27 '19 at 13:00

1 Answers1

1

Per the exchanges in the comments, the main issue appears to be how the basic authorization header was being formed.

For future readers, the format for the authorization header is:

.SetRequestHeader "Authorization", "Basic " & Base64Encode(SITE_ID & ":" & API_KEY)

Also, another issue you may run into is related here. Linebreaks are inserted into the Base64 encoded string with the current approach, which won't play nice with most (if not all) APIs. A suggested fix for this would be something like:

Public Function Base64Encode(ByVal inputText As String, Optional removeBlankLines = True) As String
    Dim xmlDoc As Object
    Dim docNode As Variant
    Set xmlDoc = CreateObject("Msxml2.DOMDocument.3.0")
    Set docNode = xmlDoc.createElement("base64")
    docNode.DataType = "bin.base64"
    docNode.nodeTypedValue = Stream_StringToBinary(inputText)
    Base64Encode = docNode.Text
    Set docNode = Nothing
    Set xmlDoc = Nothing

    'remove blank line characters ASCII --> 10,13,10 + 13
    If removeBlankLines Then Base64Encode = Replace(Replace(Replace(Base64Encode, vbCrLf, vbNullString), vbLf, vbNullString), vbCr, vbNullString)
End Function
Ryan Wildry
  • 5,612
  • 1
  • 15
  • 35