This document details how the mtd_workflowmax module integrates with the WorkflowMax API, focusing on authentication, request/response formats, and common challenges.
The module uses OAuth2 for API authentication:
from mtd_workflowmax.api.auth import OAuthManager
auth_manager = OAuthManager()
token = auth_manager.get_token()Required environment variables:
WORKFLOWMAX_CLIENT_ID=your-client-id
WORKFLOWMAX_CLIENT_SECRET=your-client-secret
WORKFLOWMAX_REFRESH_TOKEN=your-refresh-tokenBase URL: https://api.workflowmax2.com/
- Get Contact
GET client.api/contact/{uuid}
- Update Contact Custom Fields
PUT client.api/contact/{uuid}/customfield
- Get Definitions
GET customfield.api/definition
The most critical aspect of the API integration is proper XML formatting for custom field updates. The order of XML tags matters and all fields must be included.
<CustomFields>
<CustomField>
<UUID>field-uuid</UUID> <!-- UUID must come first -->
<Name>field-name</Name> <!-- Name comes second -->
<Type>field-type</Type> <!-- Type comes third -->
<Value>field-value</Value> <!-- Value comes last -->
</CustomField>
</CustomFields><!-- Wrong: Missing UUID -->
<CustomField>
<Name>field-name</Name>
<Type>field-type</Type>
<Value>field-value</Value>
</CustomField>
<!-- Wrong: Incorrect tag order -->
<CustomField>
<Name>field-name</Name>
<UUID>field-uuid</UUID>
<Type>field-type</Type>
<Value>field-value</Value>
</CustomField>Link type fields require special handling:
- Definition Format:
<CustomFieldDefinition>
<UUID>field-uuid</UUID>
<Name>LINKEDIN PROFILE</Name>
<Type>Link</Type>
<LinkURL>https://{value}</LinkURL>
</CustomFieldDefinition>- Update Format:
<CustomField>
<UUID>field-uuid</UUID>
<Name>LINKEDIN PROFILE</Name>
<Type>Link</Type>
<LinkURL>https://www.linkedin.com/in/username</LinkURL>
</CustomField>Key points:
- Preserve the full URL in updates
- Don't apply the template when sending updates
- Include the UUID from the field definition
- Authentication Error:
<Response>
<Status>Error</Status>
<ErrorDescription>Invalid access token</ErrorDescription>
</Response>- Validation Error:
<Response>
<Status>Error</Status>
<ErrorDescription>Invalid field value</ErrorDescription>
</Response>try:
response = api_client.put(endpoint, data=xml_payload)
root = ET.fromstring(response.text)
status = root.find('Status').text
if status != 'OK':
error = root.find('ErrorDescription').text
raise WorkflowMaxError(f"API error: {error}")
except ET.ParseError as e:
raise XMLParsingError(f"Invalid XML response: {str(e)}")
except requests.RequestException as e:
raise APIError(f"Request failed: {str(e)}")The API has rate limits that must be respected:
- 1000 requests per hour
- 10 requests per second
Implementation:
from time import sleep
class RateLimiter:
def __init__(self):
self.last_request = 0
self.min_interval = 0.1 # 100ms between requests
def wait(self):
now = time.time()
elapsed = now - self.last_request
if elapsed < self.min_interval:
sleep(self.min_interval - elapsed)
self.last_request = time.time()- Enable Debug Logging:
import logging
logging.getLogger('workflowmax').setLevel(logging.DEBUG)- Log Request/Response:
logger.debug(f"Request XML: {xml_payload}")
logger.debug(f"Response: {response.text}")- Common Issues:
-
XML Formatting:
# Wrong f"<Value>{value}</Value>" # Right f"<Value>{sanitize_xml(value)}</Value>"
-
Link URLs:
# Wrong link_url.replace('{value}', url) # Right url # Keep original URL format
-
Custom Field Updates:
# Wrong update_single_field(uuid, name, value) # Right update_all_fields(uuid, {name: value})
Use the provided test environment:
WORKFLOWMAX_API_URL=https://api.workflowmax2.com/test/Test data:
TEST_CONTACT_UUID = "test-contact-uuid"
TEST_CUSTOM_FIELD = {
"name": "TEST FIELD",
"type": "Text",
"value": "test value"
}- Always validate XML before sending:
def validate_xml(xml_string: str) -> bool:
try:
ET.fromstring(xml_string)
return True
except ET.ParseError:
return False- Include all required fields:
def validate_required_fields(custom_field: dict) -> None:
required = ['UUID', 'Name', 'Type']
missing = [f for f in required if f not in custom_field]
if missing:
raise ValidationError(f"Missing required fields: {missing}")- Handle special characters:
def sanitize_xml(value: str) -> str:
return value.replace('&', '&') \
.replace('<', '<') \
.replace('>', '>') \
.replace('"', '"') \
.replace("'", ''')- Verify updates:
def verify_update(uuid: str, updates: dict) -> bool:
contact = get_contact(uuid)
return all(
contact.custom_fields[name].value == value
for name, value in updates.items()
)