BackOffice Calls

Using a BackOffice method is a little different than one from Q2Api or WedgeOnlineBanking. These methods interact with HQ’s BackOffice service, which requires an authenticated BackOffice session. How you obtain that session depends on the type of extension you are building.

Console Extensions

Console extensions run within an active Console CSR User session, so self.hq_credentials is already a BackOfficeHqCredentials instance. You can call BackOffice endpoints directly:

from q2_sdk.hq.hq_api.backoffice import GetVersion

params = GetVersion.ParamsObj(self.logger, self.hq_credentials)
response = await GetVersion.execute(params)

Central Extensions

Central extensions run within an active Console or Central CSR User session, so self.hq_credentials is already a BackOfficeHqCredentials instance. Usage is identical to Console:

from q2_sdk.hq.hq_api.backoffice import GetVersion

params = GetVersion.ParamsObj(self.logger, self.hq_credentials)
response = await GetVersion.execute(params)

Online Extensions

Online extensions only have a user session token, which does not grant BackOffice access. You must explicitly establish a BackOffice session by calling get_backoffice_credentials with a CSR username and password that has permissions to log into Central:

from q2_sdk.hq.hq_api.backoffice import DecideSuspectGt

back_office_credentials = self.hq_credentials.get_backoffice_credentials(
    self.logger,
    'user_name',
    'user_pwd'
)
params = DecideSuspectGt.ParamsObj(
    self.logger,
    back_office_credentials,
    1,
    1,
    "This is a test",
    False,
)
suspect_results = await DecideSuspectGt.execute(params)

This calls LogonAdmin behind the scenes to mint a BackOffice session cookie, then uses that session for the endpoint call. Keep in mind that Central is being sunset, so this approach is not recommended for new development.

Working with Responses

Every BackOffice endpoint returns a BackOfficeResponse instance. Call .json() to get a usable Python dictionary:

from q2_sdk.hq.hq_api.backoffice import GetVersion

params = GetVersion.ParamsObj(self.logger, self.hq_credentials)
response = await GetVersion.execute(params)
data = response.json()
# {"Version": "2024.1.0.0", ...}

For endpoints that return DataSet-typed responses, .json() automatically parses the embedded XSD schema to produce properly-typed values. Table rows are always returned as lists of dicts:

from q2_sdk.hq.hq_api.backoffice import GetGroup

params = GetGroup.ParamsObj(self.logger, self.hq_credentials, group_id=1)
response = await GetGroup.execute(params)
data = response.json()
# {
#     "TransactionRights": [
#         {"TransactionTypeID": 1, "Enabled": True, ...},
#         {"TransactionTypeID": 2, "Enabled": False, ...},
#     ],
#     "Q2_Group": [
#         {"GroupID": 1, "GroupDescription": "CSR-L5", ...}
#     ],
#     "AuditId": 12345,
# }

Integer, boolean, and decimal fields are coerced to their native Python types. Single-row tables are still returned as a one-element list for consistency. Scalar fields outside of DataSet tables (like AuditId) are included as top-level keys with inferred types.

Using DataSet Parameters

Some BackOffice endpoints accept DataSet parameters, which represent typed .NET DataSet objects serialized over the wire. These endpoints default to the JSON (.ashx) transport because DataSet serialization over SOAP is not supported. Each endpoint that uses DataSets has a corresponding _models module with dataclass models and a builder function. For example, AddCustomer has AddCustomer_models.

from q2_sdk.hq.hq_api.backoffice import AddCustomer
from q2_sdk.hq.hq_api.backoffice.AddCustomer_models import (
    Q2Customer,
    Q2Address,
    build_dal_customer_edit_dataset,
)

# Build the model instances — use negative IDs for new records
customer = Q2Customer(
    customer_id=-1,
    customer_name="Jane Doe",
    group_id=1,
    create_date="2024-01-01",
)
address = Q2Address(
    address_id=-1,
    city="Austin",
    state="TX",
    postal_code="78701",
    address_type=1,
    country_id=1,
)

# Build the DataSet using the generated builder function. Only pass the
# tables you need; each builder argument accepts a single instance or a list.
dal_customer_edit = build_dal_customer_edit_dataset(
    q2_customer=customer,
    q2_address=address,
)

# Pass the DataSet to the ParamsObj and execute
params = AddCustomer.ParamsObj(
    self.logger,
    self.hq_credentials,
    dal_customer_edit=dal_customer_edit,
)
response = await AddCustomer.execute(params)

AddCustomer also accepts a second, optional customer_rights DataSet for the customer’s transaction, user, and monetary rights. You build it exactly the same way — construct the rights model instances, pass them to build_customer_rights_dataset() (also in AddCustomer_models), and hand the result to the matching customer_rights parameter:

from q2_sdk.hq.hq_api.backoffice.AddCustomer_models import (
    build_customer_rights_dataset,
)

customer_rights = build_customer_rights_dataset(
    transaction_rights=...,
    user_rights=...,
)
params = AddCustomer.ParamsObj(
    self.logger,
    self.hq_credentials,
    dal_customer_edit=dal_customer_edit,
    customer_rights=customer_rights,
)

See the endpoint’s _models module for every available table class and builder.

Updating Existing Records

Update operations that take a DataSet parameter require the current data first so HQ can perform optimistic concurrency checking: each modified row must be paired with its unmodified state (the original_record field) so HQ can emit the “Unchanged” row it compares against. (Without that Unchanged row, HQ rejects the call.) Update operations that take only plain scalar parameters do not carry a DataSet, so they have no such requirement.

You do not need to wire this up by hand. Every DataSet-based Update endpoint’s generated module exposes a fetch_current() helper (alongside execute()) that calls the matching GET operation, parses the response into typed model instances, and pre-populates original_record on each one. Modify the returned instances, build the DataSet, and execute:

from q2_sdk.hq.hq_api.backoffice import UpdateUserLogon
from q2_sdk.hq.hq_api.backoffice.UpdateUserLogon_models import (
    build_dal_user_logon_dataset,
)

# 1. Fetch current state (original_record is pre-populated on each instance)
current = await UpdateUserLogon.fetch_current(
    self.logger, self.hq_credentials, user_logon_id=12345
)

# 2. Modify the returned dataclass instance(s)
user_logon = current["Q2UserLogon"][0]
user_logon.login_name = "new_login_name"

# 3. Build the DataSet and execute. Here the ``dal_user_logon`` parameter is
#    the DalUser_Logon DataSet.
dal_user_logon = build_dal_user_logon_dataset(q2_user_logon=user_logon)
params = UpdateUserLogon.ParamsObj(
    self.logger, self.hq_credentials, dal_user_logon=dal_user_logon
)
response = await UpdateUserLogon.execute(params)

Note

Some Update endpoints take more than one DataSet parameter. For example, UpdateUserProfile accepts both dup (the user profile) and an optional user_rights DataSet, and UpdateGroup accepts group_edit plus group_rights. The pattern is the same — call fetch_current(), modify the relevant instances, then build and pass each DataSet — but the model classes and builder functions differ per endpoint. Check the endpoint’s _models module (e.g. UpdateUserProfile_models) for the exact model classes and the corresponding build_*_dataset() functions it provides (one per DataSet parameter).