Skip to content

Endpoints

api

Modules:

Classes:

  • ActualServer

    Implements the low-level API for interacting with the Actual server by just implementing the API calls and

ActualServer

ActualServer(
    base_url: str = "http://localhost:5006",
    token: str | None = None,
    password: str | None = None,
    bootstrap: bool = False,
    cert: str | SSLContext | bool = True,
    extra_headers: dict[str, str] | None = None,
    timeout: float | Timeout | None = 60.0,
)

Implements the low-level API for interacting with the Actual server by just implementing the API calls and response models.

Parameters:

  • base_url

    (str, default: 'http://localhost:5006' ) –

    Url of the running Actual server.

  • token

    (str | None, default: None ) –

    The token for authentication, if this is available (optional).

  • password

    (str | None, default: None ) –

    The password for authentication. It will be used on the .login() method to retrieve the token.

  • bootstrap

    (bool, default: False ) –

    if the server is not bootstrapped, bootstrap it with the password.

  • cert

    (str | SSLContext | bool, default: True ) –

    If a custom certificate should be used (e.g., self-signed certificate), its path can be provided as a string or as custom ssl.SSLContext. Set to False for no certificate check.

  • extra_headers

    (dict[str, str] | None, default: None ) –

    Additional headers to be attached to each request to the Actual server.

  • timeout

    (float | Timeout | None, default: 60.0 ) –

    Timeout in seconds applied to all HTTP requests. Set to None to disable. Accepts a float or an httpx.Timeout object for fine-grained control. Defaults to 60 seconds.

Methods:

  • login

    Logs in on the Actual server using the password provided. Raises AuthorizationError if it fails to

  • headers

    Generates a header based on the stored token for the connection.

  • info

    Gets the information from the Actual server, like the name and version.

  • validate

    Validates if the user is valid and logged in, and if the token is also valid and bound to a session.

  • needs_bootstrap

    Checks if the Actual needs bootstrap, in other words, if it needs a master password for the server.

  • data_file_index

    Gets all the migration file references for the actual server.

  • data_file

    Gets the content of the individual migration file from server.

  • reset_user_file

    Resets the file. If the file_id is not provided, the current file set is reset. Usually used together with

  • download_user_file

    Downloads the user file based on the file_id provided. Returns the bytes from the response, which is a

  • upload_user_file

    Uploads the binary data, which is a zip folder containing the db.sqlite and the metadata.json. If the

  • list_user_files

    Lists the user files. If the response item contains encrypt_key_id different from None, then the

  • get_user_file_info

    Gets the user file information, including the encryption metadata.

  • update_user_file_name

    Updates the file name for the budget on the remote server.

  • delete_user_file

    Deletes the user file loaded from the remote server.

  • user_get_key

    Gets the key information associated with a user file, including the algorithm, key, salt, and iv.

  • user_create_key

    Creates a new key for the user file. The key has to be used then to encrypt the local file, and this file

  • reset_password

    Resets the password for the user. You need to be logged in to reset your password, as the old

  • sync_sync

    Calls the sync endpoint with a request and returns the response. Both the request and response are

  • login_methods

    Returns login methods available for the user.

  • is_open_id_owner_created

    Checks if the owner has been created on the OpenID server. This endpoint is non-authorized, which means

  • open_id_config

    Gets the OpenID configuration for the server. You will need to provide the main password to access this

  • open_id_users

    Returns the list of OpenID users on the server.

  • create_open_id_user

    Creates a new user on the OpenID server, assigning it, by default, the most basic permissions.

  • update_open_id_user

    Updates a user on the OpenID server.

  • delete_open_id_user

    Deletes a user from the OpenID server. Will raise an exception with 404 if the user does not exist.

  • list_file_users_allowed

    Lists all users allowed to access a certain file. Also returns if the user owns the file or not.

Source code in actual/api/__init__.py
def __init__(
    self,
    base_url: str = "http://localhost:5006",
    token: str | None = None,
    password: str | None = None,
    bootstrap: bool = False,
    cert: str | ssl.SSLContext | bool = True,
    extra_headers: dict[str, str] | None = None,
    timeout: float | httpx.Timeout | None = 60.0,
):
    """
    :param base_url: Url of the running Actual server.
    :param token: The token for authentication, if this is available (optional).
    :param password: The password for authentication. It will be used on the .login() method to retrieve the token.
    :param bootstrap: if the server is not bootstrapped, bootstrap it with the password.
    :param cert: If a custom certificate should be used (e.g., self-signed certificate), its path can be provided
                 as a string or as custom [ssl.SSLContext][ssl.SSLContext]. Set to `False` for no certificate check.
    :param extra_headers: Additional headers to be attached to each request to the Actual server.
    :param timeout: Timeout in seconds applied to all HTTP requests. Set to `None` to disable. Accepts a float or
                    an `httpx.Timeout` object for fine-grained control. Defaults to 60 seconds.
    """
    self.api_url: str = base_url.rstrip("/")
    self._token: str | None = token
    if isinstance(cert, bool) or isinstance(cert, ssl.SSLContext):
        verify = cert
    else:
        verify = ssl.create_default_context()
        verify.load_verify_locations(cadata=cert)
    # todo: Rename this on the next breaking change
    self._requests_session: httpx.Client = httpx.Client(
        base_url=self.api_url, headers=extra_headers, verify=verify, timeout=timeout
    )
    if token is None and password is None and not self.is_open_id_owner_created():
        raise ValueError("Either provide a valid token or a password.")
    # already try to log in if the password was provided
    if password and bootstrap and not self.needs_bootstrap().data.bootstrapped:
        self.bootstrap(password)
    elif password:
        self.login(password)
    elif not token and self.is_open_id_owner_created():
        self.login(None, method="openid")
    # set default headers for the connection
    self._requests_session.headers.update(self.headers())
    # finally, call validate
    self.validate()

login

login(
    password: str,
    method: Literal["password", "header"] = ...,
) -> LoginDTO
login(
    password: None = ..., *, method: Literal["openid"]
) -> LoginDTO
login(
    password: str | None = None,
    method: Literal[
        "password", "header", "openid"
    ] = "password",
) -> LoginDTO

Logs in on the Actual server using the password provided. Raises AuthorizationError if it fails to authenticate the user.

Parameters:

  • password

    (str | None, default: None ) –

    password of the Actual server. If missing, OpenID authentication will be attempted.

  • method

    (Literal['password', 'header', 'openid'], default: 'password' ) –

    the method used to authenticate with the server. Check the official auth header documentation for information. Here, the appropriate method will be chosen even if this option is missing.

Raises:

  • AuthorizationError

    if the token is invalid.

Source code in actual/api/__init__.py
def login(
    self, password: str | None = None, method: Literal["password", "header", "openid"] = "password"
) -> LoginDTO:
    """
    Logs in on the Actual server using the password provided. Raises `AuthorizationError` if it fails to
    authenticate the user.

    :param password: password of the Actual server. If missing, OpenID authentication will be attempted.
    :param method: the method used to authenticate with the server. Check the [official auth header documentation](
    https://actualbudget.org/docs/advanced/http-header-auth/) for information. Here, the appropriate method will
    be chosen even if this option is missing.
    :raises AuthorizationError: if the token is invalid.
    """
    if method in ("password", "header"):
        if password is None:
            raise AuthorizationError("Trying to login but no password was provided.")
        if method == "password":
            response = self._requests_session.post(
                Endpoints.LOGIN.value,
                json={"loginMethod": method, "password": password},
            )
        else:
            response = self._requests_session.post(
                Endpoints.LOGIN.value,
                json={"loginMethod": method},
                headers={"X-ACTUAL-PASSWORD": password},
            )
    else:  # openid
        # check first if the openid server is created
        if not self.is_open_id_owner_created():
            raise AuthorizationError("OpenID server is not set-up.")

        with AuthCodeReceiver() as receiver:
            redirect_url = f"http://localhost:{receiver.get_port()}"
            response = self._requests_session.post(
                Endpoints.LOGIN.value,
                json={"loginMethod": method, "password": password, "returnUrl": redirect_url},
            )
            response.raise_for_status()
            login_response = LoginDTO.model_validate(response.json())
            auth_response = receiver.get_auth_response(auth_uri=login_response.data.return_url, timeout=60)
            if not auth_response:
                raise AuthorizationError("Could not authenticate with Open ID.")
            self._token = auth_response.get("token")
            return login_response
    if response.status_code == 400 and "invalid-password" in response.text:
        raise AuthorizationError("Could not validate password on login.")
    elif response.status_code == 200 and "invalid-header" in response.text:
        # try the same login with the header
        return self.login(password, "header")
    elif response.status_code > 400:
        raise AuthorizationError(f"Server returned an HTTP error '{response.status_code}': '{response.text}'")
    response_dict = response.json()
    if response_dict["status"] == "error":
        # for example, when not trusting the proxy
        raise AuthorizationError(f"Something went wrong on login: {response_dict['reason']}")
    login_response = LoginDTO.model_validate(response.json())
    # older versions do not return 400 but rather return empty tokens
    if login_response.data.token is None:
        raise AuthorizationError("Could not validate password on login.")
    self._token = login_response.data.token
    return login_response

headers

headers(
    file_id: str | None = None,
    extra_headers: dict | None = None,
) -> dict

Generates a header based on the stored token for the connection.

If a file_id is provided, it would be used as the X-ACTUAL-FILE-ID header. Extra headers will be included as they are provided on the final dictionary.

Source code in actual/api/__init__.py
def headers(self, file_id: str | None = None, extra_headers: dict | None = None) -> dict:
    """
    Generates a header based on the stored token for the connection.

    If a `file_id` is provided, it would be used as the `X-ACTUAL-FILE-ID` header. Extra headers will be
    included as they are provided on the final dictionary."""
    if not self._token:
        raise AuthorizationError("Token not available for requests. Use the login() method or provide a token.")
    headers = {"X-ACTUAL-TOKEN": self._token}
    if file_id:
        headers["X-ACTUAL-FILE-ID"] = file_id
    if extra_headers:
        headers.update(extra_headers)
    return headers

info

info() -> InfoDTO

Gets the information from the Actual server, like the name and version.

Source code in actual/api/__init__.py
def info(self) -> InfoDTO:
    """Gets the information from the Actual server, like the name and version."""
    response = self._requests_session.get(Endpoints.INFO.value)
    response.raise_for_status()
    return InfoDTO.model_validate(response.json())

validate

validate() -> ValidateDTO

Validates if the user is valid and logged in, and if the token is also valid and bound to a session.

Source code in actual/api/__init__.py
def validate(self) -> ValidateDTO:
    """Validates if the user is valid and logged in, and if the token is also valid and bound to a session."""
    response = self._requests_session.get(Endpoints.ACCOUNT_VALIDATE.value)
    response.raise_for_status()
    return ValidateDTO.model_validate(response.json())

needs_bootstrap

needs_bootstrap() -> BootstrapInfoDTO

Checks if the Actual needs bootstrap, in other words, if it needs a master password for the server.

Source code in actual/api/__init__.py
def needs_bootstrap(self) -> BootstrapInfoDTO:
    """Checks if the Actual needs bootstrap, in other words, if it needs a master password for the server."""
    response = self._requests_session.get(Endpoints.NEEDS_BOOTSTRAP.value)
    response.raise_for_status()
    return BootstrapInfoDTO.model_validate(response.json())

data_file_index

data_file_index() -> list[str]

Gets all the migration file references for the actual server.

Source code in actual/api/__init__.py
def data_file_index(self) -> list[str]:
    """Gets all the migration file references for the actual server."""
    response = self._requests_session.get(Endpoints.DATA_FILE_INDEX.value)
    response.raise_for_status()
    return response.content.decode().splitlines()

data_file

data_file(file_path: str) -> bytes

Gets the content of the individual migration file from server.

Source code in actual/api/__init__.py
def data_file(self, file_path: str) -> bytes:
    """Gets the content of the individual migration file from server."""
    response = self._requests_session.get(f"/data/{file_path}")
    response.raise_for_status()
    return response.content

reset_user_file

reset_user_file(file_id: str) -> StatusDTO

Resets the file. If the file_id is not provided, the current file set is reset. Usually used together with the upload_user_file() method.

Source code in actual/api/__init__.py
def reset_user_file(self, file_id: str) -> StatusDTO:
    """Resets the file. If the file_id is not provided, the current file set is reset. Usually used together with
    the upload_user_file() method."""
    if file_id is None:
        raise UnknownFileId("Could not reset the file without a valid 'file_id'")
    request = self._requests_session.post(
        Endpoints.RESET_USER_FILE.value, json={"fileId": file_id, "token": self._token}
    )
    request.raise_for_status()
    return StatusDTO.model_validate(request.json())

download_user_file

download_user_file(file_id: str) -> bytes

Downloads the user file based on the file_id provided. Returns the bytes from the response, which is a zipped folder of the database db.sqlite and the metadata.json. If the database is encrypted, the key id has to be retrieved additionally using user_get_key().

Source code in actual/api/__init__.py
def download_user_file(self, file_id: str) -> bytes:
    """Downloads the user file based on the file_id provided. Returns the `bytes` from the response, which is a
    zipped folder of the database `db.sqlite` and the `metadata.json`. If the database is encrypted, the key id
    has to be retrieved additionally using user_get_key()."""
    db = self._requests_session.get(Endpoints.DOWNLOAD_USER_FILE.value, headers=self.headers(file_id))
    db.raise_for_status()
    return db.content

upload_user_file

upload_user_file(
    binary_data: bytes,
    file_id: str,
    file_name: str = "My Finances",
    encryption_meta: dict | None = None,
) -> UploadUserFileDTO

Uploads the binary data, which is a zip folder containing the db.sqlite and the metadata.json. If the file is encrypted, the encryption_meta has to be provided with fields keyId, algorithm, iv and authTag

Source code in actual/api/__init__.py
def upload_user_file(
    self, binary_data: bytes, file_id: str, file_name: str = "My Finances", encryption_meta: dict | None = None
) -> UploadUserFileDTO:
    """Uploads the binary data, which is a zip folder containing the `db.sqlite` and the `metadata.json`. If the
    file is encrypted, the encryption_meta has to be provided with fields `keyId`, `algorithm`, `iv` and `authTag`
    """
    base_headers = {
        "X-ACTUAL-FORMAT": "2",
        "X-ACTUAL-FILE-ID": file_id,
        "X-ACTUAL-NAME": file_name,
        "Content-Type": "application/encrypted-file",
    }
    if encryption_meta:
        base_headers["X-ACTUAL-ENCRYPT-META"] = json.dumps(encryption_meta)
    request = self._requests_session.post(
        Endpoints.UPLOAD_USER_FILE.value,
        content=binary_data,
        headers=self.headers(extra_headers=base_headers),
    )
    request.raise_for_status()
    return UploadUserFileDTO.model_validate(request.json())

list_user_files

list_user_files() -> ListUserFilesDTO

Lists the user files. If the response item contains encrypt_key_id different from None, then the file must be decrypted on retrieval.

Source code in actual/api/__init__.py
def list_user_files(self) -> ListUserFilesDTO:
    """Lists the user files. If the response item contains `encrypt_key_id` different from `None`, then the
    file must be decrypted on retrieval."""
    response = self._requests_session.get(Endpoints.LIST_USER_FILES.value)
    response.raise_for_status()
    return ListUserFilesDTO.model_validate(response.json())

get_user_file_info

get_user_file_info(file_id: str) -> GetUserFileInfoDTO

Gets the user file information, including the encryption metadata.

Source code in actual/api/__init__.py
def get_user_file_info(self, file_id: str) -> GetUserFileInfoDTO:
    """Gets the user file information, including the encryption metadata."""
    response = self._requests_session.get(Endpoints.GET_USER_FILE_INFO.value, headers=self.headers(file_id))
    response.raise_for_status()
    return GetUserFileInfoDTO.model_validate(response.json())

update_user_file_name

update_user_file_name(
    file_id: str, file_name: str
) -> StatusDTO

Updates the file name for the budget on the remote server.

Source code in actual/api/__init__.py
def update_user_file_name(self, file_id: str, file_name: str) -> StatusDTO:
    """Updates the file name for the budget on the remote server."""
    response = self._requests_session.post(
        Endpoints.UPDATE_USER_FILE_NAME.value,
        json={"fileId": file_id, "name": file_name, "token": self._token},
    )
    response.raise_for_status()
    return StatusDTO.model_validate(response.json())

delete_user_file

delete_user_file(file_id: str)

Deletes the user file loaded from the remote server.

Source code in actual/api/__init__.py
def delete_user_file(self, file_id: str):
    """Deletes the user file loaded from the remote server."""
    response = self._requests_session.post(
        Endpoints.DELETE_USER_FILE.value, json={"fileId": file_id, "token": self._token}
    )
    return StatusDTO.model_validate(response.json())

user_get_key

user_get_key(file_id: str) -> UserGetKeyDTO

Gets the key information associated with a user file, including the algorithm, key, salt, and iv.

Source code in actual/api/__init__.py
def user_get_key(self, file_id: str) -> UserGetKeyDTO:
    """Gets the key information associated with a user file, including the algorithm, key, salt, and iv."""
    response = self._requests_session.post(
        Endpoints.USER_GET_KEY.value,
        json={
            "fileId": file_id,
            "token": self._token,
        },
        headers=self.headers(file_id),
    )
    response.raise_for_status()
    return UserGetKeyDTO.model_validate(response.json())

user_create_key

user_create_key(
    file_id: str, key_id: str, password: str, key_salt: str
) -> StatusDTO

Creates a new key for the user file. The key has to be used then to encrypt the local file, and this file still needs to be uploaded.

Source code in actual/api/__init__.py
def user_create_key(self, file_id: str, key_id: str, password: str, key_salt: str) -> StatusDTO:
    """Creates a new key for the user file. The key has to be used then to encrypt the local file, and this file
    still needs to be uploaded."""
    key = create_key_buffer(password, key_salt)
    test_content = make_test_message(key_id, key)
    response = self._requests_session.post(
        Endpoints.USER_CREATE_KEY.value,
        json={
            "fileId": file_id,
            "keyId": key_id,
            "keySalt": key_salt,
            "testContent": json.dumps(test_content),
            "token": self._token,
        },
    )
    return StatusDTO.model_validate(response.json())

reset_password

reset_password(new_password: str) -> StatusDTO

Resets the password for the user. You need to be logged in to reset your password, as the old password does not need to be provided.

Source code in actual/api/__init__.py
def reset_password(self, new_password: str) -> StatusDTO:
    """Resets the password for the user. You need to be logged in to reset your password, as the old
    password does not need to be provided."""
    response = self._requests_session.post(
        Endpoints.RESET_PASSWORD.value,
        json={"password": new_password, "token": self._token},
    )
    return StatusDTO.model_validate(response.json())

sync_sync

sync_sync(request: SyncRequest) -> SyncResponse

Calls the sync endpoint with a request and returns the response. Both the request and response are protobuf models. The request and response are not standard REST, but rather protobuf binary serialized data. The server stores this serialized data to allow the user to replay all changes to the database and construct a local copy.

Source code in actual/api/__init__.py
def sync_sync(self, request: SyncRequest) -> SyncResponse:
    """Calls the sync endpoint with a request and returns the response. Both the request and response are
    protobuf models. The request and response are not standard REST, but rather protobuf binary serialized data.
    The server stores this serialized data to allow the user to replay all changes to the database and construct
    a local copy."""
    response = self._requests_session.post(
        Endpoints.SYNC.value,
        headers=self.headers(request.fileId, extra_headers={"Content-Type": "application/actual-sync"}),
        content=SyncRequest.serialize(request),
    )
    response.raise_for_status()
    parsed_response = SyncResponse.deserialize(response.content)
    return parsed_response  # noqa

login_methods

login_methods() -> LoginMethodsDTO

Returns login methods available for the user.

Source code in actual/api/__init__.py
def login_methods(self) -> LoginMethodsDTO:
    """Returns login methods available for the user."""
    response = self._requests_session.get(Endpoints.LOGIN_METHODS.value)
    response.raise_for_status()
    return LoginMethodsDTO.model_validate(response.json())

is_open_id_owner_created

is_open_id_owner_created() -> bool

Checks if the owner has been created on the OpenID server. This endpoint is non-authorized, which means you can access it even if the user is not logged in.

Source code in actual/api/__init__.py
def is_open_id_owner_created(self) -> bool:
    """Checks if the owner has been created on the OpenID server. This endpoint is non-authorized, which means
    you can access it even if the user is not logged in."""
    response = self._requests_session.get(Endpoints.OPEN_ID_OWNER_CREATED.value)
    if response.status_code > 400:
        # here, it could be that the method returns 404 for an older version
        return False
    return response.json()

open_id_config

open_id_config(password: str) -> OpenIDConfigResponseDTO

Gets the OpenID configuration for the server. You will need to provide the main password to access this config.

Source code in actual/api/__init__.py
def open_id_config(self, password: str) -> OpenIDConfigResponseDTO:
    """Gets the OpenID configuration for the server. You will need to provide the main password to access this
    config."""
    response = self._requests_session.post(Endpoints.OPEN_ID_CONFIG.value, json={"password": password})
    response.raise_for_status()
    return OpenIDConfigResponseDTO.model_validate(response.json())

open_id_users

open_id_users() -> list[OpenIDUserDTO]

Returns the list of OpenID users on the server.

Source code in actual/api/__init__.py
def open_id_users(self) -> list[OpenIDUserDTO]:
    """Returns the list of OpenID users on the server."""
    response = self._requests_session.get(Endpoints.OPEN_ID_USERS.value)
    response.raise_for_status()
    return [OpenIDUserDTO.model_validate(entry) for entry in response.json()]

create_open_id_user

create_open_id_user(
    user_name: str,
    display_name: str = "",
    enabled: bool = True,
    owner: bool = False,
    role: Literal["ADMIN", "BASIC"] = "BASIC",
) -> OpenIDUserDTO

Creates a new user on the OpenID server, assigning it, by default, the most basic permissions.

Source code in actual/api/__init__.py
def create_open_id_user(
    self,
    user_name: str,
    display_name: str = "",
    enabled: bool = True,
    owner: bool = False,
    role: Literal["ADMIN", "BASIC"] = "BASIC",
) -> OpenIDUserDTO:
    """Creates a new user on the OpenID server, assigning it, by default, the most basic permissions."""
    payload = {
        "id": "",
        "userName": user_name,
        "displayName": display_name,
        "enabled": enabled,
        "owner": owner,
        "role": role,
    }
    response = self._requests_session.post(Endpoints.OPEN_ID_USERS.value, json=payload)
    response.raise_for_status()
    model_response = OpenIDUserDTO.model_validate(payload)
    # fill entity since the endpoint does not return a DTO
    model_response.id = response.json()["data"]["id"]
    return model_response

update_open_id_user

update_open_id_user(
    user_id: str,
    user_name: str | None = None,
    display_name: str | None = None,
    enabled: bool | None = None,
    owner: bool | None = None,
    role: Literal["ADMIN", "BASIC"] | None = None,
) -> OpenIDUserDTO

Updates a user on the OpenID server.

Source code in actual/api/__init__.py
def update_open_id_user(
    self,
    user_id: str,
    user_name: str | None = None,
    display_name: str | None = None,
    enabled: bool | None = None,
    owner: bool | None = None,
    role: Literal["ADMIN", "BASIC"] | None = None,
) -> OpenIDUserDTO:
    """Updates a user on the OpenID server."""
    users = self.open_id_users()
    payload = [user for user in users if user.id == user_id]
    if not payload:
        raise ActualInvalidOperationError(f"Could not find user with id {user_id}")
    user = payload[0]
    if user_name:
        user.user_name = user_name
    if display_name:
        user.display_name = display_name
    if enabled is not None:
        user.enabled = enabled
    if owner is not None:
        user.owner = owner
    if role is not None:
        user.role = role
    elif user.role is None:
        user.role = "BASIC"  # seems like a bug from actual
    response = self._requests_session.patch(Endpoints.OPEN_ID_USERS.value, json=user.model_dump(by_alias=True))
    response.raise_for_status()
    return user

delete_open_id_user

delete_open_id_user(
    user_id: str,
) -> OpenIDDeleteUserResponseDTO

Deletes a user from the OpenID server. Will raise an exception with 404 if the user does not exist.

Source code in actual/api/__init__.py
def delete_open_id_user(self, user_id: str) -> OpenIDDeleteUserResponseDTO:
    """Deletes a user from the OpenID server. Will raise an exception with 404 if the user does not exist."""
    # Use .request instead of .delete since httpx doesn't support payload on delete
    response = self._requests_session.request("DELETE", Endpoints.OPEN_ID_USERS.value, json={"ids": [user_id]})
    response.raise_for_status()
    return OpenIDDeleteUserResponseDTO.model_validate(response.json())

list_file_users_allowed

list_file_users_allowed(
    file_id: str,
) -> list[OpenIDUserFileAccessDTO]

Lists all users allowed to access a certain file. Also returns if the user owns the file or not.

Source code in actual/api/__init__.py
def list_file_users_allowed(self, file_id: str) -> list[OpenIDUserFileAccessDTO]:
    """Lists all users allowed to access a certain file. Also returns if the user owns the file or not."""
    response = self._requests_session.get(Endpoints.OPEN_ID_ACCESS_USERS.value, params={"fileId": file_id})
    response.raise_for_status()
    return [OpenIDUserFileAccessDTO.model_validate(entry) for entry in response.json()]

models

Classes:

Endpoints

Bases: Enum

List of all endpoints mapped by the Actualpy API.

BankSyncs

Bases: Enum

Types of bank sync supported by the library.

Attributes:

GOCARDLESS class-attribute instance-attribute

GOCARDLESS = 'gocardless'

GoCardless integration. See how to set it up

SIMPLEFIN class-attribute instance-attribute

SIMPLEFIN = 'simplefin'

Simplefin integration. See how to set it up

StatusCode

Bases: Enum

Status code of the request response.

Attributes:

  • OK

    Ok response code.

  • ERROR

    Error response code.

OK class-attribute instance-attribute

OK = 'ok'

Ok response code.

ERROR class-attribute instance-attribute

ERROR = 'error'

Error response code.

StatusDTO

Bases: BaseModel

Base class for all status responses.

The general classes will contain the status code and the data model under data property.

Parameters:

ErrorStatusDTO

Bases: BaseModel

Parameters:

  • status

    (StatusCode) –
  • reason

    (str | None, default: None ) –

TokenDTO

Bases: BaseModel

Response model for the token on login.

Here, if you try to log in with a password, you will get a token, and if you try to log in with an OpenID, you will get a return_url.

Parameters:

  • token

    (str | None, default: None ) –
  • return_url

    (str | None, default: None ) –

LoginDTO

Bases: StatusDTO

Login response model.

Parameters:

UploadUserFileDTO

Bases: StatusDTO

Upload user file response model.

Parameters:

IsValidatedDTO

Bases: BaseModel

Response model for the validation of a budget file.

Parameters:

  • validated

    (bool | None) –
  • user_name

    (str | None, default: None ) –
  • permission

    (str | None, default: None ) –
  • user_id

    (str | None, default: None ) –
  • display_name

    (str | None, default: None ) –
  • login_method

    (str | None, default: 'password' ) –

ValidateDTO

Bases: StatusDTO

Validate budget response model.

Parameters:

EncryptMetaDTO

Bases: BaseModel

Encryption metadata.

Parameters:

  • key_id

    (str | None) –
  • algorithm

    (str | None) –
  • iv

    (str | None) –
  • auth_tag

    (str | None) –

EncryptionTestDTO

Bases: BaseModel

Encryption test data including the encryption metadata.

Parameters:

EncryptionDTO

Bases: BaseModel

Encryption information including the salt and test data (with encryption metadata).

Parameters:

  • id

    (str | None) –
  • salt

    (str | None) –
  • test

    (str | None) –

FileDTO

Bases: BaseModel

Base file model.

Parameters:

  • deleted

    (int | None) –
  • file_id

    (str) –
  • group_id

    (str | None) –
  • name

    (str) –

RemoteFileListDTO

Bases: FileDTO

Remote file model.

If the file is encrypted, the encrypt_key_id field will be present.

If OpenID is enabled and the file is owned by certain users, the owner field will be present.

Parameters:

  • deleted

    (int | None) –
  • file_id

    (str) –
  • group_id

    (str | None) –
  • name

    (str) –
  • encrypt_key_id

    (str | None) –
  • owner

    (str | None, default: None ) –
  • users_with_access

    (ForwardRef('list[BaseOpenIDUserFileAccessDTO] | None'), default: <dynamic> ) –

    Built-in mutable sequence.

    If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.

RemoteFileDTO

Bases: FileDTO

Remote file model (including encryption metadata).

Parameters:

GetUserFileInfoDTO

Bases: StatusDTO

Get user file info response model.

Parameters:

ListUserFilesDTO

Bases: StatusDTO

List user files response model.

Parameters:

UserGetKeyDTO

Bases: StatusDTO

User key response model.

Parameters:

BuildDTO

Bases: BaseModel

Build information from the Actual server, including the name, version and description.

Parameters:

  • name

    (str) –
  • description

    (str | None) –
  • version

    (str | None) –

InfoDTO

Bases: BaseModel

Information response model.

Parameters:

LoginMethodDTO

Bases: BaseModel

Login method information.

Parameters:

  • method

    (str) –
  • active

    (bool) –
  • display_name

    (str) –

IsBootstrapedDTO

Bases: BaseModel

Bootstrap information, including available login methods and whether multi-user is enabled.

Parameters:

  • bootstrapped

    (bool) –
  • login_method

    (str | None, default: 'password' ) –
  • multi_user

    (bool | None, default: False ) –
  • available_login_methods

    (list[LoginMethodDTO] | None, default: None ) –

LoginMethodsDTO

Bases: StatusDTO

Login methods response model.

Parameters:

BootstrapInfoDTO

Bases: StatusDTO

Bootstrap information response model.

Parameters:

IsConfiguredDTO

Bases: BaseModel

Bank status configuration status (configured is True if configured).

Parameters:

  • configured

    (bool) –

BankSyncStatusDTO

Bases: StatusDTO

Bank sync status response model.

Parameters:

BankSyncAccountDTO

Bases: StatusDTO

Bank sync account response model.

Parameters:

BankSyncTransactionResponseDTO

Bases: StatusDTO

Bank sync transaction response model.

Parameters:

BankSyncErrorDTO

Bases: StatusDTO

Bank sync error response model.

Parameters:

IssuerConfig

Bases: BaseModel

OpenID issuer configuration.

Parameters:

  • name

    (str) –

    Friendly name for the issuer

  • authorization_endpoint

    (str) –

    Authorization endpoint URL

  • token_endpoint

    (str) –

    Token endpoint URL

  • userinfo_endpoint

    (str) –

    User info endpoint URL

OpenIDConfigDTO

Bases: BaseModel

OpenID configuration.

Parameters:

  • doc

    (str, default: 'OpenID authentication settings.' ) –

    Documentation string

  • discovery_url

    (str | None) –
  • issuer

    (IssuerConfig | None) –
  • client_id

    (str) –
  • client_secret

    (str) –
  • server_hostname

    (str) –
  • auth_method

    (Literal['openid', 'oauth2']) –

OpenIDConfigResponseDTO

Bases: StatusDTO

OpenID configuration response model.

Parameters:

OpenIDBootstrapDTO

Bases: BaseModel

OpenID bootstrap configuration.

Parameters:

  • client_id

    (str) –

    OAuth2 client ID

  • client_secret

    (str) –

    OAuth2 client secret

  • discovery_url

    (IssuerConfig | None, default: None ) –

    OpenID discovery URL

  • server_hostname

    (str) –

OpenIDUserDTO

Bases: BaseModel

OpenID user information.

Parameters:

  • id

    (str) –
  • user_name

    (str) –
  • display_name

    (str | None) –
  • enabled

    (bool) –
  • owner

    (bool) –
  • role

    (str | None) –

    User role (ADMIN or BASIC)

OpenIDDeleteUserDTO

Bases: BaseModel

OpenID user deletion information.

Parameters:

  • some_deletions_failed

    (bool) –

OpenIDDeleteUserResponseDTO

Bases: StatusDTO

OpenID user deletion response model.

Parameters:

BaseOpenIDUserFileAccessDTO

Bases: BaseModel

Base OpenID user file access information.

Parameters:

  • user_id

    (str) –
  • user_name

    (str) –
  • display_name

    (str | None) –
  • owner

    (bool) –

OpenIDUserFileAccessDTO

Bases: BaseOpenIDUserFileAccessDTO

OpenID user file access information.

Parameters:

  • user_id

    (str) –
  • user_name

    (str) –
  • display_name

    (str | None) –
  • owner

    (bool) –
  • have_access

    (bool) –

bank_sync

Classes:

BankSyncTransactionDTO

Bases: BaseModel

Parameters:

  • id

    (str) –
  • posted

    (int) –
  • amount

    (str) –
  • description

    (str) –
  • payee

    (str) –
  • memo

    (str) –

BankSyncOrgDTO

Bases: BaseModel

Parameters:

  • domain

    (str) –
  • sfin_url

    (str) –

BankSyncAccountDTO

Bases: BaseModel

Parameters:

BankSyncAmount

Bases: BaseModel

Parameters:

  • amount

    (Decimal) –
  • currency

    (str) –

DebtorAccount

Bases: BaseModel

Parameters:

  • iban

    (str) –

Balance

Bases: BaseModel

An object containing the balance amount and currency.

Parameters:

  • balance_amount

    (BankSyncAmount) –
  • balance_type

    (BalanceType) –
  • reference_date

    (str | None, default: None ) –

    The date of the balance

TransactionItem

Bases: BaseModel

Parameters:

  • transaction_id

    (str | None, default: None ) –
  • booked

    (bool | None, default: False ) –
  • transaction_amount

    (BankSyncAmount) –
  • payee_name

    (str | None, default: None ) –
  • date

    (date) –
  • notes

    (str | None, default: None ) –
  • payee

    (str | None, default: None ) –
  • payee_account

    (DebtorAccount | None, default: None ) –
  • booking_date

    (date | None, default: None ) –
  • value_date

    (date | None, default: None ) –
  • remittance_information_unstructured

    (str | None, default: None ) –
  • remittance_information_unstructured_array

    (list[str], default: <dynamic> ) –

    Built-in mutable sequence.

    If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.

  • additional_information

    (str | None, default: None ) –
  • posted_date

    (date | None, default: None ) –

Attributes:

  • imported_payee

    Deprecated method to convert the payee name. Use the payee_name instead.

imported_payee property

imported_payee

Deprecated method to convert the payee name. Use the payee_name instead.

Transactions

Bases: BaseModel

Parameters:

BankSyncAccountData

Bases: BaseModel

Parameters:

BankSyncTransactionData

Bases: BaseModel

Parameters:

  • balances

    (list[Balance]) –
  • starting_balance

    (int) –
  • transactions

    (Transactions) –
  • iban

    (str | None, default: None ) –
  • institution_id

    (str | None, default: None ) –

Attributes:

  • balance (Decimal) –

    Starting balance of the account integration, converted to a decimal amount.

balance property

balance: Decimal

Starting balance of the account integration, converted to a decimal amount.

For simpleFin, this will represent the current amount on the account, while for goCardless it will represent the actual initial amount before all transactions.

BankSyncErrorData

Bases: BaseModel

Parameters:

  • error_type

    (str) –
  • error_code

    (str) –
  • status

    (str | None, default: None ) –
  • reason

    (str | None, default: None ) –

protobuf_models

Classes:

  • HULC_Client

    A Hybrid Unique Logical Clock (HULC) timestamp generator.

  • EncryptedData

    The encrypted data information, namely the iv, authTag and data.

  • Message

    A change message from Actual, containing the dataset (table), row (primary key), column and value.

  • MessageEnvelope

    Envelopes a message while including the timestamp and if the message is encrypted or not.

  • SyncRequest

    Sync request message that is sent to the server for retrieving new messages since the last synchronization.

  • SyncResponse

    Sync response that is sent to the client with the new messages.

HULC_Client

HULC_Client(
    client_id: str | None = None,
    initial_count: int = 0,
    ts: datetime | None = None,
)

A Hybrid Unique Logical Clock (HULC) timestamp generator.

The generator makes sure that change timestamps are consistent across multiple clients that could have different clocks.

Methods:

  • from_timestamp

    Generates a HULC_Client from a timestamp string.

  • timestamp

    Actual uses Hybrid Unique Logical Clock (HULC) timestamp generator.

  • random_client_id

    Creates a client id for the HULC request.

Source code in actual/protobuf_models.py
def __init__(self, client_id: str | None = None, initial_count: int = 0, ts: datetime.datetime | None = None):
    self.client_id = client_id or self.random_client_id()
    self.initial_count = initial_count
    self.ts = ts

from_timestamp classmethod

from_timestamp(ts: str) -> HULC_Client

Generates a HULC_Client from a timestamp string.

Source code in actual/protobuf_models.py
@classmethod
def from_timestamp(cls, ts: str) -> HULC_Client:
    """Generates a HULC_Client from a timestamp string."""
    ts_string, _, rest = ts.partition("Z")
    segments = rest.split("-")
    parsed_ts = datetime.datetime.fromisoformat(ts_string)
    return cls(segments[-1], int(segments[-2], 16), parsed_ts)

timestamp

timestamp(now: datetime | None = None) -> str

Actual uses Hybrid Unique Logical Clock (HULC) timestamp generator.

Timestamps serialize into a 46-character collatable string. Examples:

  • 2015-04-24T22:23:42.123Z-1000-0123456789ABCDEF
  • 2015-04-24T22:23:42.123Z-1000-A219E7A71CC18912

See original source code for reference.

Source code in actual/protobuf_models.py
def timestamp(self, now: datetime.datetime | None = None) -> str:
    """Actual uses Hybrid Unique Logical Clock (HULC) timestamp generator.

    Timestamps serialize into a 46-character collatable string. Examples:

     - `2015-04-24T22:23:42.123Z-1000-0123456789ABCDEF`
     - `2015-04-24T22:23:42.123Z-1000-A219E7A71CC18912`

    See [original source code](
    https://github.com/actualbudget/actual/blob/a9362cc6f9b974140a760ad05816cac51c849769/packages/crdt/src/crdt/timestamp.ts)
    for reference.
    """
    current_time = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) if now is None else now
    # truncate to millisecond precision to match the Node.js Date.now() behavior
    current_time = current_time.replace(microsecond=current_time.microsecond // 1000 * 1000)
    # ensure that the logical time never goes backward
    new_logical_time = current_time if self.ts is None else max(self.ts, current_time)
    # advance the counter if same millisecond, otherwise reset to 0
    new_counter = self.initial_count + 1 if (self.ts is not None and self.ts == new_logical_time) else 0

    if new_counter > self.MAX_COUNTER:
        raise ActualOverflowError(
            f"Timestamp counter overflow (>{self.MAX_COUNTER}). "
            "Too many sync messages were generated without the clock advancing."
        )

    self.ts = new_logical_time
    self.initial_count = new_counter
    return str(self)

random_client_id staticmethod

random_client_id()

Creates a client id for the HULC request.

Implementation copied from the source code.

Source code in actual/protobuf_models.py
@staticmethod
def random_client_id():
    """Creates a client id for the HULC request.

    Implementation copied [from the source code](
    https://github.com/actualbudget/actual/blob/a9362cc6f9b974140a760ad05816cac51c849769/packages/crdt/src/crdt/timestamp.ts#L80).
    """
    return str(uuid.uuid4()).replace("-", "")[-16:]

EncryptedData

Bases: Message

The encrypted data information, namely the iv, authTag and data.

Message

Bases: Message

A change message from Actual, containing the dataset (table), row (primary key), column and value.

Methods:

  • get_value

    Serialization types from Actual.

  • set_value

    Sets the value of the message based on the Actual spec for datatypes.

get_value

get_value() -> str | float | None

Serialization types from Actual.

Original source code

Source code in actual/protobuf_models.py
def get_value(self) -> str | float | None:
    """Serialization types from Actual.

    [Original source code](
    https://github.com/actualbudget/actual/blob/998efb9447da6f8ce97956cbe83d6e8a3c18cf53/packages/loot-core/src/server/sync/index.ts#L154-L160)
    """
    datatype, _, value = self.value.partition(":")
    if datatype == "S":
        return value
    elif datatype == "N":
        return float(value)
    elif datatype == "0":
        return None
    else:
        raise ValueError(f"Conversion not supported for datatype '{datatype}'")

set_value

set_value(value: str | int | float | None) -> str

Sets the value of the message based on the Actual spec for datatypes.

Original source code

Source code in actual/protobuf_models.py
def set_value(self, value: str | int | float | None) -> str:
    """
    Sets the value of the message based on the Actual spec for datatypes.

    [Original source code](
    https://github.com/actualbudget/actual/blob/998efb9447da6f8ce97956cbe83d6e8a3c18cf53/packages/loot-core/src/server/sync/index.ts#L154-L160)
    """
    if isinstance(value, str):
        datatype = "S"
    elif isinstance(value, int) or isinstance(value, float):
        datatype = "N"
    elif value is None:
        datatype = "0"
    else:
        raise ValueError(f"Conversion not supported for datatype '{type(value)}'")
    self.value = f"{datatype}:{value}"
    return self.value

MessageEnvelope

Bases: Message

Envelopes a message while including the timestamp and if the message is encrypted or not.

SyncRequest

Bases: Message

Sync request message that is sent to the server for retrieving new messages since the last synchronization.

SyncResponse

Bases: Message

Sync response that is sent to the client with the new messages.