Access

Usando la API REST de QuickBooks a través de VBA – Parte 1

Como muchos saben, he publicado un par de artículos sobre la implementación de API REST usando VBA:

  • API REST REST de Outlook
  • API de Google REST
  • Monday.com REST API

Una vez que obtenga el concepto básico y el marco construido, se vuelve relativamente simple implementar casi cualquier API REST (¡siempre que la documentación sea buena!).

Entonces, una vez más, haciendo un trabajo de investigación para una aplicación web para un cliente, me pidieron que analizara la integración de QuickBooks a través de PHP. Una vez que lo hice, decidí mirar si se pudiera hacer lo mismo en VBA usando mi marco básico.

En primer lugar, no sé si soy solo yo (y podría ser), pero en comparación con Monday.com, Google, Xero, … por alguna razón, he encontrado que QuickBooks es más difícil de ponerse en funcionamiento inicialmente funcional. ¡Su parque infantil API apesta! Especialmente cuando lo compara con el Explorador de API Graph de Microsoft o el Playground API de Monday.com. ¡También me resultó más difícil localizar la documentación inicialmente y recurrir mucho más a las búsquedas en línea!

Dicho esto, una vez que sepa, los conceptos básicos siguen siendo los mismos y el proceso es relativamente sencillo.

Descripción general del proceso.

  1. Consulta ‘con su credencial para obtener un código de autorización
  2. Con esa consulta del código de autorización ‘para obtener un token y un token de actualización
  3. El uso de lo siguiente para consultar a su empresa.
    • ‘Para entorno de sandboxed/de desarrollo
    • ‘t.com/v3/company/’ para la producción
Limitación de los tokens de QuickBooks
  • El token de acceso es válido por 1 hora.
  • El token de actualización es válido por 100 días, pero se restablece cada vez que regenere/actualiza el token de acceso para que sea fundamental capturarlo siempre cuando lo haga.

Para este proceso, debe haber establecido una cuenta y tener su:

Para el proceso de autenticación

  • ID de cliente
  • Redirigir URL
    • ‘Mientras estén en desarrollo
    • ‘Lo que sea que hayas definido’ cuando estén en producción
  • Secreto del cliente

Para trabajar con la API REST y su empresa

  • Token de acceso generado por el proceso de autenticación anterior
  • ID de reino

Mi marco típico

En el pasado, para Say Google o MS, he podido usar el control del navegador web (heredado) para realizar el proceso de autenticación. Sin embargo, la URL de autenticación de QuickBooks no se presenta correctamente en el WBC y solo funciona en el control moderno del navegador web.

En este caso, quería proporcionar una solución a la audiencia más amplia posible, por lo que opté por generar la URL a través del código y presionarla a mi navegador web para obtener el código de autenticación para que luego obtenga el token para las operaciones normales, pero si desea usar fácilmente mi marco típico de API de Google/Microsoft con el MWBC para hacer la autenticación dentro de un formulario de acceso.

La estructura de la tabla

En mi configuración, elegí tener 2 tablas:

  1. QB_ACCOUNTINFO
    Esto se utiliza para almacenar la información básica de la cuenta (ID del cliente, secreto, redirigir URI, ID de reino y código de autenticación)
  2. QB_OAUTH2
    Esto se utiliza para almacenar el acceso y actualizar los tokens

La estructura básica de las tablas es:

CREATE TABLE QB_OAuth2 (
    EntryID AUTOINCREMENT PRIMARY KEY,
    client_id TEXT(255) NOT NULL,
    client_secret TEXT(255) NOT NULL,
    scope TEXT(255) NOT NULL,
    access_token LONGTEXT,
    refresh_token TEXT(255),
    token_type TEXT(255),
    token_creation DATETIME,
    expires_in INTEGER
);

CREATE TABLE QB_AccountInfo (
    EntryID AUTOINCREMENT PRIMARY KEY,
    client_id TEXT(255) NOT NULL,
    client_secret TEXT(255) NOT NULL,
    redirect_uri TEXT(255) NOT NULL,
    realm_id TEXT(255),
    auth_code Text(255)
);

Mi módulo básico de QuickBooks

Ahora hay muchas formas de atacar esto, pero para este ejemplo elegí crear un módulo y construir una serie de procedimientos.

Mi módulo está compuesto por los siguientes procedimientos:

  • QB_GETAUTHCODE
  • QB_Token_get
  • QB_Token_Refresh
  • QB_Token_Revoke
  • QB_Taken_ISValid
  • OAUTH2_ACCOUNTINFO_LOAD
  • OAUTH2_ACCOUNTINFO_AUTHCODE_CLEAR
  • OAuth2_Token_Save
  • Oauth2_token_load
  • OAUTH2_TOKIN_CLEAR
  • GenerateCacheBuster
  • Código urlen
  • Parsejson2
Public Const bQBDebugMode = True
Public Const sAccountTable = "QB_AccountInfo"
Public Const sCredentialTable = "QB_OAuth2"
'Public Const sAuthenticationForm = "QB_Authentication"

Type Auth
    auth_code                 As String
    access_token              As String
    client_id                 As String
    client_secret             As String
    redirect_uri              As String
    realm_id                  As String
    expires_in                As Long
    refresh_token             As String
    scope                     As String
    token_creation            As Date
    token_type                As String
End Type
Public OAuth2                 As Auth


Sub QB_GetAuthCode()
    Dim sClientID             As String
    Dim sRedirectURI          As String
    Dim sScope                As String
    Dim sEndPoint             As String
    Dim sAuthURL              As String

    If OAuth2.client_id = "" Then Call OAuth2_AccountInfo_Load
    sClientID = OAuth2.client_id
    sRedirectURI = OAuth2.redirect_uri
    sScope = "com.intuit.quickbooks.accounting"    'Could be a Procedure Argument

    sEndPoint = "
    sAuthURL = sEndPoint & "?" & _
               "client_id=" & sClientID & _
               "&redirect_uri=" & sRedirectURI & _
               "&response_type=code" & _
               "&scope=" & sScope & _
               "&state=" & "state_" & Format(Now, "yyyymmddhhmmss") & "_" & Rnd()
    If bQBDebugMode Then Debug.Print "QB_GetAuthCode: " & sAuthURL
    Application.FollowHyperlink sAuthURL    'For Access
    'Shell "cmd /c start " & authURL, vbHide 'More generic

    MsgBox "Complete authorization in the browser, then enter the Auth Code and Realm ID in the form."

    DoCmd.OpenForm "QB_AccountInfo", acNormal, , , , acDialog
End Sub

Function QB_Token_Get() As Boolean
    Dim oHTTP                 As Object
    Dim sResponse             As String
    Dim sStatus               As String
    Dim sClientID             As String
    Dim sClientSecret         As String
    Dim sRedirectURI          As String
    Dim sAuthCode As String
    Dim sEndPoint             As String
    Dim sRequest              As String

    Call OAuth2_AccountInfo_Load 'Refresh to ensure we get the AuthCode
    sClientID = OAuth2.client_id
    sClientSecret = OAuth2.client_secret
    sRedirectURI = OAuth2.redirect_uri
    sAuthCode = OAuth2.auth_code
    
    If sAuthCode = "" Then
        MsgBox "No Auth Code!"
        Exit Function
    End If

    sEndPoint = "
    sRequest = "grant_type=authorization_code" & _
               "&code=" & sAuthCode & _
               "&redirect_uri=" & sRedirectURI

    Set oHTTP = CreateObject("MSXML2.ServerXMLHTTP.6.0")
    With oHTTP
        .Open "POST", sEndPoint, False
        .setRequestHeader "Authorization", "Basic " & EncodeBase64(sClientID & ":" & sClientSecret)
        .setRequestHeader "Content-Type", "application/x-www-form-urlencoded"
        .send sRequest

        sResponse = .responseText
        sStatus = .Status
    End With

    If sStatus = 200 Then
        Debug.Print "New Access Token: " & ParseJson2(sResponse, "access_token")
        Debug.Print "New Refresh Token: " & ParseJson2(sResponse, "refresh_token")
        
        OAuth2.access_token = ParseJson2(sResponse, "access_token")
        OAuth2.refresh_token = ParseJson2(sResponse, "refresh_token")
        Call OAuth2_Token_Save
        
        QB_Token_Get = True
    Else
        Debug.Print "Token Exchange Failed: " & sResponse
    End If

    Set oHTTP = Nothing
End Function

Function QB_Token_Refresh() As Boolean
    Dim oHTTP                 As Object
    Dim sResponse             As String
    Dim sStatus               As String
    Dim sClientID             As String
    Dim sClientSecret         As String
    Dim sRedirectURI          As String
    Dim sRefreshToken         As String
    Dim sAuthorizationHeader  As String
    Dim sEndPoint             As String
    Dim sRequest              As String

    ' Initialize values
    Call OAuth2_AccountInfo_Load
    sClientID = OAuth2.client_id
    sClientSecret = OAuth2.client_secret
    Call OAuth2_Token_Load
    sRefreshToken = OAuth2.refresh_token

    sEndPoint = "
    sRequest = "grant_type=refresh_token" & _
               "&refresh_token=" & sRefreshToken
    sAuthorizationHeader = "Basic " & EncodeBase64(sClientID & ":" & sClientSecret)

    Set oHTTP = CreateObject("MSXML2.ServerXMLHTTP.6.0")
    With oHTTP
        .Open "POST", sEndPoint, False
        .setRequestHeader "Authorization", sAuthorizationHeader
        .setRequestHeader "Content-Type", "application/x-www-form-urlencoded"
        .setRequestHeader "Accept", "application/json"
        .send sRequest

        sResponse = .responseText
        sStatus = .Status
    End With

    If sStatus = 200 Then
        Debug.Print "New Access Token: " & ParseJson2(sResponse, "access_token")
        Debug.Print "New Refresh Token: " & ParseJson2(sResponse, "refresh_token")

        OAuth2.access_token = ParseJson2(sResponse, "access_token")
        OAuth2.refresh_token = ParseJson2(sResponse, "refresh_token")
        Call OAuth2_Token_Save

        QB_Token_Refresh = True
    Else
        Debug.Print "Error: " & sResponse
    End If

    Set oHTTP = Nothing
End Function

Function QB_Token_Revoke() As Boolean
    Dim oHTTP                 As Object    'New MSXML2.ServerXMLHTTP60
    Dim sResponse             As String
    Dim sStatus               As String
    Dim sClientID             As String
    Dim sClientSecret         As String
    Dim sRefreshToken         As String
    Dim sEndPoint             As String
    Dim sAuthorizationHeader  As String
    Dim sRequest              As String

    Call OAuth2_AccountInfo_Load
    sClientID = OAuth2.client_id
    sClientSecret = OAuth2.client_secret
    Call OAuth2_Token_Load
    sRefreshToken = OAuth2.refresh_token

    sEndPoint = "
    sAuthorizationHeader = "Basic " & EncodeBase64(sClientID & ":" & sClientSecret)
    sRequest = "token=" & sRefreshToken

    Set oHTTP = CreateObject("MSXML2.ServerXMLHTTP.6.0")
    With oHTTP
        .Open "POST", sEndPoint, False
        .setRequestHeader "Authorization", sAuthorizationHeader
        .setRequestHeader "Content-Type", "application/x-www-form-urlencoded"
        .send sRequest

        sResponse = .responseText
        sStatus = .Status
    End With

    If sStatus = 200 Then
        Debug.Print "Token revoked successfully."
        
        'Wipe credentials from tables!
        Call OAuth2_Token_Clear
        Call OAuth2_AccountInfo_AuthCode_Clear
        
        QB_Token_Revoke = True
    Else
        Debug.Print "Error: " & sResponse
    End If
    Set oHTTP = Nothing
End Function

Function QB_Token_IsValid() As Boolean
    Call OAuth2_Token_Load
    If OAuth2.token_creation = "12:00:00 AM" Then Exit Function
    If DateAdd("s", OAuth2.expires_in, OAuth2.token_creation) > Now Then QB_Token_IsValid = True
End Function

Public Function OAuth2_AccountInfo_Load()
    On Error GoTo Error_Handler
    Dim rs                    As DAO.Recordset

    Set rs = oCurrentDb.OpenRecordset("SELECT * FROM (" & sAccountTable & ")", dbOpenSnapshot)
    If rs.RecordCount > 0 Then
        With OAuth2
            .client_id = Nz(rs!(client_id))
            .client_secret = Nz(rs!(client_secret))
            .redirect_uri = Nz(rs!(redirect_uri))
            .realm_id = Nz(rs!(realm_id))
            .auth_code = Nz(rs!(auth_code))
        End With
    End If

Error_Handler_Exit:
    On Error Resume Next
    rs.Close
    Set rs = Nothing
    Exit Function

Error_Handler:
    MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _
           "Error Source: OAuth2_StoredCredentials_Load" & vbCrLf & _
           "Error Number: " & Err.Number & vbCrLf & _
           "Error Description: " & Err.Description & _
           Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
           , vbOKOnly + vbCritical, "An Error has Occurred!"
    Resume Error_Handler_Exit
End Function

Public Sub OAuth2_AccountInfo_AuthCode_Clear()
    On Error GoTo Error_Handler
    Dim rs                    As DAO.Recordset

    Set rs = oCurrentDb.OpenRecordset("SELECT * FROM (" & sAccountTable & ")")
    With rs
        If .RecordCount <> 0 Then
            .Edit
            !(auth_code) = Null
            .Update
        End If
    End With

    OAuth2.auth_code = ""
    
Error_Handler_Exit:
    On Error Resume Next
    rs.Close
    Set rs = Nothing
    Exit Sub

Error_Handler:
    MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _
           "Error Source: OAuth2_AccountInfo_AuthCode_Clear" & vbCrLf & _
           "Error Number: " & Err.Number & vbCrLf & _
           "Error Description: " & Err.Description & _
           Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
           , vbOKOnly + vbCritical, "An Error has Occurred!"
    Resume Error_Handler_Exit
End Sub

Public Sub OAuth2_Token_Save()
    On Error GoTo Error_Handler
    Dim rs                    As DAO.Recordset

    Set rs = oCurrentDb.OpenRecordset("SELECT * FROM (" & sCredentialTable & ")")
    With rs
        If .RecordCount = 0 Then
            .AddNew
        Else
            .Edit
        End If
        !(access_token) = Nz(OAuth2.access_token) 'Expires in 1 hour!
        !(refresh_token) = Nz(OAuth2.refresh_token) 'Expires in 100 days, but gets renewed when new Access Token is issued
        !(token_creation) = Now '#4/6/2025 1:22:00 PM#
        !(expires_in) = 3600
        .Update
    End With

Error_Handler_Exit:
    On Error Resume Next
    rs.Close
    Set rs = Nothing
    Exit Sub

Error_Handler:
    MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _
           "Error Source: OAuth2_Token_Save" & vbCrLf & _
           "Error Number: " & Err.Number & vbCrLf & _
           "Error Description: " & Err.Description & _
           Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
           , vbOKOnly + vbCritical, "An Error has Occurred!"
    Resume Error_Handler_Exit
End Sub

Public Function OAuth2_Token_Load()
    On Error GoTo Error_Handler
    Dim rs                    As DAO.Recordset

    Set rs = oCurrentDb.OpenRecordset("SELECT * FROM (" & sCredentialTable & ")", dbOpenSnapshot)
    If rs.RecordCount > 0 Then
        With OAuth2
            .access_token = Nz(rs!(access_token))
            .refresh_token = Nz(rs!(refresh_token))
            .token_creation = Nz(rs!(token_creation))
            .expires_in = Nz(rs!(expires_in), 0)
        End With
    End If

Error_Handler_Exit:
    On Error Resume Next
    rs.Close
    Set rs = Nothing
    Exit Function

Error_Handler:
    MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _
           "Error Source: OAuth2_Token_Load" & vbCrLf & _
           "Error Number: " & Err.Number & vbCrLf & _
           "Error Description: " & Err.Description & _
           Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
           , vbOKOnly + vbCritical, "An Error has Occurred!"
    Resume Error_Handler_Exit
End Function

Public Sub OAuth2_Token_Clear()
    On Error GoTo Error_Handler
    Dim rs                    As DAO.Recordset

    Set rs = oCurrentDb.OpenRecordset("SELECT * FROM (" & sCredentialTable & ")")
    With rs
        If .RecordCount <> 0 Then
            .Edit
            !(access_token) = Null
            !(refresh_token) = Null
            !(token_creation) = Null
            !(expires_in) = Null
'            !(scope) = Null
'            !(token_type) = Null
            .Update
        End If
    End With

'    OAuth2.client_id = ""
'    OAuth2.client_secret = ""
    OAuth2.access_token = ""
    OAuth2.refresh_token = ""
    OAuth2.token_creation = "12:00:00 AM"
    OAuth2.expires_in = 0
'    OAuth2.scope = ""
'    OAuth2.token_type = ""

Error_Handler_Exit:
    On Error Resume Next
    rs.Close
    Set rs = Nothing
    Exit Sub

Error_Handler:
    MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _
           "Error Source: OAuth2_Token_Clear" & vbCrLf & _
           "Error Number: " & Err.Number & vbCrLf & _
           "Error Description: " & Err.Description & _
           Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
           , vbOKOnly + vbCritical, "An Error has Occurred!"
    Resume Error_Handler_Exit
End Sub

Function GenereateCacheBuster() As String
    GenereateCacheBuster = "cachebuster=" & Format(Now, "yyyymmddhhmmss") & "_" & Rnd()
End Function

Function URLEncode(ByVal Text As String) As String
    Dim i                     As Integer
    Dim Char                  As String
    Dim EncodedText           As String

    ' Loop through each character in the input text
    For i = 1 To Len(Text)
        Char = Mid(Text, i, 1)

        ' Check if the character is alphanumeric or one of these safe characters: - _ . ~
        If Char Like "(A-Za-z0-9-_.~)" Then
            EncodedText = EncodedText & Char
        Else
            ' Convert unsafe characters to % followed by their ASCII value in hex
            EncodedText = EncodedText & "%" & Right("0" & Hex(Asc(Char)), 2)
        End If
    Next i

    URLEncode = EncodedText
End Function

Function ParseJson2(json As String, key As String) As String
    ' Simple JSON parser for this specific use case
    Dim regex As Object
    Set regex = CreateObject("VBScript.RegExp")
    regex.Pattern = """(" & key & ")"":""((^"")+)"""
    If regex.Test(json) Then
        ParseJson2 = regex.Execute(json)(0).SubMatches(1)
    End If
End Function

Empezando

Lo primero que debe hacer es poblar la tabla QB_AccountInfo con la información de su empresa de QuickBooks:

  • ID de cliente
  • Secreto del cliente

Paso 1

El procedimiento QB_GETAUTHCODE () iniciará su navegador web, donde deberá autenticarse utilizando su nombre de usuario y contraseña para ingresar a QuickBooks (esta vez). Después de hacerlo, se le puede pedir que autorice el acceso a su empresa a su aplicación y finalmente será redirigido a una página que le proporcionará un código de autorización e ID de reino. Debe copiar esos valores y pegarlos en la tabla QB_ACCountInfo.

Paso 2

Ahora, después de que estemos listos para que nuestros tokens realmente comenzaran a trabajar con nuestros datos de QuickBooks. Para hacer esto, simplemente necesitamos ejecutar el procedimiento QB_Token_Get (). Recuperará los datos necesarios de la tabla QB_ACCountInfo y los pasará a QuickBooks y solicitará tokens. Si tiene éxito, los registrará en el QB_OAuth2 para todas las futuras llamadas API.

Una vez que tenga sus tokens, puede ejecutar indefinidamente siempre que use el sistema una vez cada 100 días, que el vencimiento del token de actualización. Pero siempre que lo use dentro de los 100 días, continuará actualizando el token de acceso, lo que le permite trabajar sin necesidad de autenticarse nunca más. ¡Esta es la belleza de las fichas!

JSON VS XML

Simplemente quería mencionar que en mis ejemplos trabajaré con JSON, pero puede trabajar igualmente con XML si lo prefiere. Verá, controle el tipo de respuesta que QuickBooks le proporciona cuando establece el encabezado Aceptar en su llamada API.

Solicitando JSON

.setRequestHeader "Accept", "application/json"

Solicitando XML

.setRequestHeader "Accept", "application/xml"

Estallido de caché

Como descubrí por las malas, el cliente HTTP parece almacenar información y, por lo tanto, si realiza la misma llamada, puede recibir los mismos datos que su llamada anterior, incluso si los datos cambiaban. Hay un par de enfoques que se pueden emplear para trabajar en torno a este problema.

Un enfoque es agregar una cadena única a cada solicitud, por lo que la solicitud es única y, por lo tanto, no existe un caché para esa solicitud exacta. Esto se conoce como caché. Es por eso que tengo y empleo un procedimiento llamado GenerateCacheBuster. Entonces, si ves que agrego GenerateCacheBuster () al final de ciertos puntos finales, ¡es por esa razón!

Posibles áreas de mejora

Lo primero que sugeriría a cualquiera que siga este camino es … Primero obtenga el marco básico funcional. Debería ser bastante sencillo. Una vez que esté en funcionamiento, recomendaría implementar variables de objetos de autouración (SHOV) para los objetos utilizados en todo el código (Base de datos, HTTP, …).

Si realmente desea llevar las cosas al siguiente nivel, especialmente para la Parte 2 de esta serie, sería crear un procedimiento de solicitud de envío HTTP como el que he usado anteriormente con las API de Google y Microsoft REST, ya que al hacerlo, simplificaría la codificación de procedimientos posteriores utilizados para funcionar con sus datos QuickBooks.

Además, muchos de mis procedimientos carecen de manejo de errores, esto se desarrolló rápidamente como una prueba de concepto y para compartir con usted, e idealmente debe agregarlo a cada procedimiento.

Por último, recomiendo encriptar la información que se almacena en las tablas de 2 QuickBooks para protegerlas de los ojos indiscretas. Obviamente, todavía está protegido de la autenticación inicial porque necesitan conocer su nombre de usuario y contraseña, pero si ya ha autenticado y recuperado tokens válidos, si alguien puede obtenerlos de su tabla, puede hacer cualquier cosa que deseen con sus datos.

Algunos recursos sobre el tema

Más por venir …

Operaciones CRUD

Publicaciones relacionadas

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Botón volver arriba