Migrate from afs-create-sf-lead
This guide walks you through migrating your integration from the legacy afs-create-sf-lead REST API to the new Marketing Lead Automation Service (MLAS) GraphQL API.
Quick Migration Checklist
Use this checklist to track your migration progress:
- Obtain new MLAS credentials (Client ID, Client Secret, Tenant ID, Scope) from Alleviate
- Implement OAuth 2.0 Client Credentials token flow (replaces the old
tokenheader) - Update your endpoint URL to the MLAS GraphQL endpoint
- Reformat your payload from a flat JSON body to a GraphQL
createLeadmutation with variables - Map your existing fields to the new MLAS field names (see Field Mapping below)
- Add the new required field:
dateOfBirth - Test end-to-end in the Sandbox environment
- Switch to the Production endpoint when ready to go live
What’s Changing
Here is a high-level summary of the key differences between the old and new services.
API Style & Endpoint
| Old Service | New Service (MLAS) | |
|---|---|---|
| Protocol | REST (POST) | GraphQL |
| Sandbox URL | N/A (single endpoint) | https://leads-sandbox.alleviate.com/graphql |
| Production URL | .../lead_creation/ | https://leads.alleviate.com/graphql |
Authentication & Payload
| Old Service | New Service (MLAS) | |
|---|---|---|
| Auth Method | Custom encrypted token header | Microsoft Entra ID OAuth 2.0 Bearer token |
| Content-Type | application/json | application/json |
| Payload Format | Flat JSON body, snake_case fields | GraphQL mutation, camelCase variables |
Behavior
| Old Service | New Service (MLAS) | |
|---|---|---|
| Lead Flow | Single POST request | Single createLead mutation returning completed workflow results |
| Testing | Admintesting field in payload | Use the dedicated Sandbox environment |
| CRM / Convoso | dtc, list_id, lead_crm fields | CRM submit path is internally configured based on partner requirements |
Step 1: Get Your New Credentials
The old service used a single encrypted token passed in a custom header. MLAS uses industry-standard OAuth 2.0 Client Credentials via Microsoft Entra ID.
You will receive the following credentials during onboarding:
| Credential | Description |
|---|---|
| Tenant ID | Alleviate’s Azure AD tenant identifier |
| Client ID | Your application’s unique identifier |
| Client Secret | Your application’s secret key |
| Debt Core App ID | Used to construct the API scope (api://{DEBT_CORE_APP_ID}/.default) |
Environment-Specific Values
The Tenant ID is the same across all environments. The Debt Core App ID differs per environment:
| Value | Sandbox | Production |
|---|---|---|
| Tenant ID | 8797fa3f-ba90-4187-97fb-6ab892ea9035 | 8797fa3f-ba90-4187-97fb-6ab892ea9035 |
| Debt Core App ID | aba793eb-f395-4f86-be59-7a7d0c1bfdb0 | 9060a1d0-da3b-4676-ad64-ff64465cce41 |
For full authentication setup details, see the Authentication page.
Step 2: Update Your Endpoint URL
Replace the old REST endpoint with the MLAS GraphQL endpoint.
| Environment | URL |
|---|---|
| Sandbox | https://leads-sandbox.alleviate.com/graphql |
| Production | https://leads.alleviate.com/graphql |
For more details on environments, see the Environments page.
Step 3: Update Authentication
The old service required a custom encrypted token in the token header:
# Old service -- custom token headercurl -X POST "https://afs-create-sf-leads.bravetree-4acef9d0.westus2.azurecontainerapps.io/lead_creation/" \ -H "token: YOUR_ENCRYPTED_TOKEN" \ -H "Content-Type: application/json" \ -d '{ ... }'MLAS uses a standard OAuth 2.0 Bearer token in the Authorization header. You first obtain an access token from Microsoft Entra ID, then include it in every request:
Use the same authentication flow across languages: request an Entra ID token, then pass it as Bearer in the GraphQL request.
curl -X POST "https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id={CLIENT_ID}" \ -d "client_secret={CLIENT_SECRET}" \ -d "scope=api://{DEBT_CORE_APP_ID}/.default"curl -X POST "https://leads-sandbox.alleviate.com/graphql" \ -H "Authorization: Bearer {ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "query": "...", "variables": { ... } }'import requests
# Step 1: Get an access tokentoken_url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token"token_response = requests.post( token_url, data={ "grant_type": "client_credentials", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "scope": f"api://{DEBT_CORE_APP_ID}/.default", },)token_response.raise_for_status()access_token = token_response.json()["access_token"]
# Step 2: Use the token in your GraphQL requestheaders = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json",}
graphql_response = requests.post( "https://leads-sandbox.alleviate.com/graphql", json={"query": "...", "variables": {}}, headers=headers,)graphql_response.raise_for_status()print(graphql_response.json())const axios = require('axios');
// Step 1: Get an access tokenconst tokenUrl = `https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`;const params = new URLSearchParams({ grant_type: 'client_credentials', client_id: CLIENT_ID, client_secret: CLIENT_SECRET, scope: `api://${DEBT_CORE_APP_ID}/.default`});
const tokenResponse = await axios.post(tokenUrl, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }});const accessToken = tokenResponse.data.access_token;
// Step 2: Use the token in your GraphQL requestconst graphqlResponse = await axios.post( 'https://leads-sandbox.alleviate.com/graphql', { query: '...', variables: {} }, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } });
console.log(graphqlResponse.data);import axios from 'axios';
type TokenResponse = { access_token: string;};
const tokenUrl = `https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`;const params = new URLSearchParams({ grant_type: 'client_credentials', client_id: CLIENT_ID, client_secret: CLIENT_SECRET, scope: `api://${DEBT_CORE_APP_ID}/.default`,});
// Step 1: Get an access tokenconst tokenResponse = await axios.post<TokenResponse>(tokenUrl, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' },});const accessToken = tokenResponse.data.access_token;
// Step 2: Use the token in your GraphQL requestconst graphqlResponse = await axios.post( 'https://leads-sandbox.alleviate.com/graphql', { query: '...', variables: {} }, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, });
console.log(graphqlResponse.data);grant_type=client_credentials&client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}&scope=api://{DEBT_CORE_APP_ID}/.default{ "query": "mutation CreateLead($input: LeadInput!) { createLead(input: $input) { id resultCode note partnerLeadId bidValue } }", "variables": { "input": {} }}Step 4: Update Your Payload
The biggest change is moving from a flat REST JSON body to a GraphQL mutation with typed variables.
Old Payload (REST)
import requestsimport json
url = "https://afs-create-sf-leads.bravetree-4acef9d0.westus2.azurecontainerapps.io/lead_creation/"
payload = json.dumps({ "offer_code": "abc1234566", "first_name": "John", "last_name": "Doe", "address": "123 Meridian Ave", "city": "Irvine", "state": "CA", "zip_code": "92618", "home_phone": "7148319225", "cell_phone": "7148319225", "email": "john.doe@example.com", "loan_amount": "72456", "marketing_lead_source": "VENDOR NAME", "lead_source": "Online Form", "est_debt": "100", "dtc": "yes", "list_id": "123", "Admintesting": "true", "sub_id_1": "tracking-123", "trustedform_certificate_id": "https://cert.trustedform.com/abcd1234"})
headers = { "token": "YOUR_ENCRYPTED_TOKEN", "Content-Type": "application/json"}
response = requests.post(url, headers=headers, data=payload)print(response.json())New Payload (GraphQL)
Use the same GraphQL mutation payload across languages. Only the HTTP client syntax changes. Note that the full list of available fields can be found on the API reference page.
curl -X POST "https://leads-sandbox.alleviate.com/graphql" \ -H "Authorization: Bearer {ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "query": "mutation CreateLead($input: LeadInput!) { createLead(input: $input) { id resultCode note partnerLeadId bidValue } }", "variables": { "input": { "firstName": "John", "lastName": "Doe", "dateOfBirth": "1990-01-15", "email": "john.doe@example.com", "homePhone": "7148319225", "cellPhone": "7148319225", "address1": "123 Meridian Ave", "city": "Irvine", "state": "CA", "zipCode": "92618", "loanAmount": 72456, "leadId": "tracking-123", "trustedformCertificateID": "https://cert.trustedform.com/abcd1234" } } }'import requests
url = "https://leads-sandbox.alleviate.com/graphql"
body = { "query": """ mutation CreateLead($input: LeadInput!) { createLead(input: $input) { id resultCode note partnerLeadId bidValue } } """, "variables": { "input": { "firstName": "John", "lastName": "Doe", "dateOfBirth": "1990-01-15", "email": "john.doe@example.com", "homePhone": "7148319225", "cellPhone": "7148319225", "address1": "123 Meridian Ave", "city": "Irvine", "state": "CA", "zipCode": "92618", "loanAmount": 72456, "leadId": "tracking-123", "trustedformCertificateID": "https://cert.trustedform.com/abcd1234" } }}
headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json"}
response = requests.post(url, json=body, headers=headers)print(response.json())const axios = require('axios');
const url = 'https://leads-sandbox.alleviate.com/graphql';
const body = { query: ` mutation CreateLead($input: LeadInput!) { createLead(input: $input) { id resultCode note partnerLeadId bidValue } } `, variables: { input: { firstName: 'John', lastName: 'Doe', dateOfBirth: '1990-01-15', email: 'john.doe@example.com', homePhone: '7148319225', cellPhone: '7148319225', address1: '123 Meridian Ave', city: 'Irvine', state: 'CA', zipCode: '92618', loanAmount: 72456, leadId: 'tracking-123', trustedformCertificateID: 'https://cert.trustedform.com/abcd1234' } }};
const headers = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json'};
const response = await axios.post(url, body, { headers });console.log(response.data);import axios from 'axios';
type CreateLeadPayload = { query: string; variables: { input: { firstName: string; lastName: string; dateOfBirth: string; email: string; homePhone: string; cellPhone: string; address1: string; city: string; state: string; zipCode: string; loanAmount: number; leadId: string; trustedformCertificateID: string; }; };};
const url = 'https://leads-sandbox.alleviate.com/graphql';
const body: CreateLeadPayload = { query: ` mutation CreateLead($input: LeadInput!) { createLead(input: $input) { id resultCode note partnerLeadId bidValue } } `, variables: { input: { firstName: 'John', lastName: 'Doe', dateOfBirth: '1990-01-15', email: 'john.doe@example.com', homePhone: '7148319225', cellPhone: '7148319225', address1: '123 Meridian Ave', city: 'Irvine', state: 'CA', zipCode: '92618', loanAmount: 72456, leadId: 'tracking-123', trustedformCertificateID: 'https://cert.trustedform.com/abcd1234' } }};
const response = await axios.post(url, body, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }});
console.log(response.data);{ "query": "mutation CreateLead($input: LeadInput!) { createLead(input: $input) { id resultCode note partnerLeadId bidValue } }", "variables": { "input": { "firstName": "John", "lastName": "Doe", "dateOfBirth": "1990-01-15", "email": "john.doe@example.com", "homePhone": "7148319225", "cellPhone": "7148319225", "address1": "123 Meridian Ave", "city": "Irvine", "state": "CA", "zipCode": "92618", "loanAmount": 72456, "leadId": "tracking-123", "trustedformCertificateID": "https://cert.trustedform.com/abcd1234" } }}Field Mapping
Use this table to map your existing old-service fields to the new MLAS GraphQL input fields.
Direct Field Mappings
These fields exist in both services. Note the change from snake_case to camelCase naming.
| Old Field (afs-create-sf-lead) | New Field (MLAS LeadInput) | Type Change | Notes |
|---|---|---|---|
first_name | firstName | String → String | No change in type |
last_name | lastName | String → String | No change in type |
address | address1 | String → String | Renamed; MLAS also supports address2 for apt/suite |
city | city | String → String | No change |
state | state | String → String | Two-letter code (e.g., "CA") |
zip_code | zipCode | String → String | Five-digit format |
home_phone | homePhone | String → String | 10-digit format, no country code |
cell_phone | cellPhone | String → String | 10-digit format, no country code |
email | email | String → String | No change |
loan_amount | loanAmount | String → Int | Now an integer in whole dollars (e.g., 72456 not "72456") |
trustedform_certificate_id | trustedformCertificateID | String → String | TrustedForm certificate UUID or URL |
est_debt | estimatedDebt | String → Int | Now an integer in whole dollars (e.g., 72456 not "72456") |
New Required Fields
These fields are required in MLAS but did not exist in the old service:
| New Field | Type | Description |
|---|---|---|
dateOfBirth | String! | Applicant’s date of birth in YYYY-MM-DD format (e.g., "1990-01-15") |
Removed / Internally Handled Fields
These old-service fields are no longer part of your request. MLAS handles them automatically or they are no longer applicable:
| Old Field | What Happens in MLAS |
|---|---|
offer_code | No longer needed. Tracking is handled via leadId, ref, and authenticated partner identity. |
marketing_lead_source | Automatically determined from your authenticated application credentials. |
lead_source | Replaced by leadSourceId, which is automatically set from your authenticated app identity. |
dtc | Not applicable. Convoso routing is handled internally by MLAS. |
list_id | Not applicable. Convoso list assignment is handled internally by MLAS. |
Admintesting | Not applicable. Use the Sandbox environment for testing instead of a flag in the payload. |
lead_crm | Not applicable. CRM routing (Salesforce vs Sigma) is handled internally by MLAS. |
Tracking / Sub-ID Mappings
| Old Field | Suggested MLAS Equivalent | Notes |
|---|---|---|
sub_id_1 through sub_id_5 | leadId, ref | Use leadId for your primary external tracking ID and ref for a secondary reference. These values are returned in responses as partnerLeadId. |
Additional Fields Available in MLAS
MLAS supports many more fields than the old service for enhanced lead qualification. See the full Lead Input Schema for details. Key optional additions include:
| Field | Type | Description |
|---|---|---|
socialSecurityNumber | String | Required for credit pull and qualification (9 digits, no dashes) |
grossAnnualIncome | Int | Gross annual income in whole dollars |
monthlyIncome | Int | Monthly income in whole dollars |
residenceTypeId | Int | Home Owner (1), Rent (2), Living with Family (3), Other (4) |
incomeSourceId | Int | Employed (1), Social Security (2), Pension (3), etc. |
loanPurposeId | Int | Debt Consolidation (1), Home Improvement (2), etc. |
termsConsent | Boolean | Must be true for lead to be processed |
coApplicant | Boolean | Set to true and populate co-applicant fields if applicable |
Step 5: Understand the New Lead Flow
Old Flow
POST /lead_creation/ → Lead is created and submitted to CRM → Response with CRM recordNew Flow
POST /graphql (createLead mutation) → Lead is processed and qualified → Response with final resultcreateLead handles deduplication, credit pull, and qualification, then returns the completed result.
CRM submission behavior is partner-configuration based:
- Some partners have automatic CRM submission for qualified leads.
- Others use manual
submitLeadfor qualified leads (1013-1017).
Here is a complete example:
import requests
BASE_URL = "https://leads-sandbox.alleviate.com/graphql"headers = {"Authorization": f"Bearer {access_token}","Content-Type": "application/json"}
# Submit the lead — processing and qualification happen before response
response = requests.post(BASE_URL, json={"query": """mutation CreateLead($input: LeadInput!) {createLead(input: $input) {idresultCodenotepartnerLeadIdbidValue}}""","variables": {"input": {"firstName": "John","lastName": "Doe","dateOfBirth": "1990-01-15","email": "john.doe@example.com","homePhone": "7148319225","cellPhone": "7148319225","address1": "123 Meridian Ave","city": "Irvine","state": "CA","zipCode": "92618","loanAmount": 72456,"leadId": "your-tracking-id-123","trustedformCertificateID": "https://cert.trustedform.com/abcd1234"}}}, headers=headers)
result = response.json()lead = result["data"]["createLead"]print(f"Lead ID: {lead['id']}")print(f"Result Code: {lead['resultCode']}")print(f"Note: {lead['note']}")print(f"Partner Lead ID: {lead['partnerLeadId']}")print(f"Bid Value: {lead['bidValue']}")const axios = require('axios');
const BASE_URL = 'https://leads-sandbox.alleviate.com/graphql';const headers = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json'};
// Submit the lead — processing and qualification happen before responseconst response = await axios.post(BASE_URL, { query: ` mutation CreateLead($input: LeadInput!) { createLead(input: $input) { id resultCode note partnerLeadId bidValue } } `, variables: { input: { firstName: 'John', lastName: 'Doe', dateOfBirth: '1990-01-15', email: 'john.doe@example.com', homePhone: '7148319225', cellPhone: '7148319225', address1: '123 Meridian Ave', city: 'Irvine', state: 'CA', zipCode: '92618', loanAmount: 72456, leadId: 'your-tracking-id-123', trustedformCertificateID: 'https://cert.trustedform.com/abcd1234' } }}, { headers });
const lead = response.data.data.createLead;console.log(`Lead ID: ${lead.id}`);console.log(`Result Code: ${lead.resultCode}`);console.log(`Note: ${lead.note}`);console.log(`Partner Lead ID: ${lead.partnerLeadId}`);console.log(`Bid Value: ${lead.bidValue}`);import axios from 'axios';
interface LeadInput { firstName: string; lastName: string; dateOfBirth: string; email: string; homePhone: string; cellPhone: string; address1: string; city: string; state: string; zipCode: string; loanAmount: number; leadId: string; trustedformCertificateID: string;}
interface CreateLeadResponse { data: { createLead: { id: string; resultCode: number; note: string; partnerLeadId: string; bidValue: string | null; }; };}
const BASE_URL = 'https://leads-sandbox.alleviate.com/graphql';
const CREATE_LEAD_MUTATION = ` mutation CreateLead($input: LeadInput!) { createLead(input: $input) { id resultCode note partnerLeadId bidValue } }`;
const submitLead = async (accessToken: string): Promise<void> => { const input: LeadInput = { firstName: 'John', lastName: 'Doe', dateOfBirth: '1990-01-15', email: 'john.doe@example.com', homePhone: '7148319225', cellPhone: '7148319225', address1: '123 Meridian Ave', city: 'Irvine', state: 'CA', zipCode: '92618', loanAmount: 72456, leadId: 'your-tracking-id-123', trustedformCertificateID: 'https://cert.trustedform.com/abcd1234', };
// Submit the lead — processing and qualification happen before response const response = await axios.post<CreateLeadResponse>( BASE_URL, { query: CREATE_LEAD_MUTATION, variables: { input }, }, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, } );
const lead = response.data.data.createLead; console.log(`Lead ID: ${lead.id}`); console.log(`Result Code: ${lead.resultCode}`); console.log(`Note: ${lead.note}`); console.log(`Partner Lead ID: ${lead.partnerLeadId}`); console.log(`Bid Value: ${lead.bidValue}`);};curl -X POST "https://leads-sandbox.alleviate.com/graphql" \ -H "Authorization: Bearer {ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "query": "mutation CreateLead($input: LeadInput!) { createLead(input: $input) { id resultCode note partnerLeadId bidValue } }", "variables": { "input": { "firstName": "John", "lastName": "Doe", "dateOfBirth": "1990-01-15", "email": "john.doe@example.com", "homePhone": "7148319225", "cellPhone": "7148319225", "address1": "123 Meridian Ave", "city": "Irvine", "state": "CA", "zipCode": "92618", "loanAmount": 72456, "leadId": "your-tracking-id-123", "trustedformCertificateID": "https://cert.trustedform.com/abcd1234" } } }'Response Format Changes
Old Response (Success)
{ "response": { "recordId": "a1MUl000001RM5VMAW" }, "status": 200, "c_result": { "status": 200, "message": 51167 }}New Response (createLead — Success)
{ "data": { "createLead": { "id": "d5faed99-51c3-4181-959e-5557d1eef042", "resultCode": 1020, "note": "Lead processing complete", "partnerLeadId": "your-tracking-id-123", "bidValue": "$25.00" } }}Key Response Fields
| Field | Description |
|---|---|
id | Unique internal MLAS lead identifier |
resultCode | Final processing status code (see Result Codes) |
note | Human-readable description of the result |
partnerLeadId | Your leadId echoed back for correlation |
bidValue | Calculated bid amount for qualified leads (null if not qualified) |
Error Handling Changes
Old Error Responses
| Scenario | Old Response |
|---|---|
| Invalid token | {"status": {"code": 401, "message": "Token is invalid."}, "response": []} |
| Missing fields | {"response": "Insert failed. First exception on row 0; ...", "status": 400} |
New Error Responses
MLAS returns standard GraphQL error responses:
{ "errors": [ { "message": "Unauthorized", "extensions": { "code": "UNAUTHENTICATED" } } ]}{ "errors": [ { "message": "Variable \"$input\" got invalid value ...", "extensions": { "code": "BAD_USER_INPUT" } } ]}Frequently Asked Questions
Do I still need to set Admintesting for test leads?
No. Use the Sandbox environment (https://leads-sandbox.alleviate.com/graphql) for all testing. The sandbox is completely isolated from production. There is no need for a test flag in the payload.
Do I still control Convoso routing with dtc and list_id?
No. Convoso routing and list assignment are now handled internally by MLAS. You do not need to include these fields.
What happened to lead_source and marketing_lead_source?
These are now automatically determined from your authenticated application credentials. When you authenticate with your Client ID, MLAS knows your partner identity and assigns the correct lead source.
What happened to offer_code?
This field is no longer needed. Use the leadId field to pass your own tracking/reference ID, and ref for a secondary identifier. Both are returned in responses for correlation.
How do I handle the loan_amount type change?
The old service accepted loan_amount as a string (e.g., "72456"). MLAS expects loanAmount as an integer (e.g., 72456). Remove any string wrapping and pass the numeric value directly.
Why is dateOfBirth now required?
MLAS performs credit-based qualification and scoring as part of the lead processing pipeline. Date of birth is essential for this process.
How do I map my sub_id values?
Use leadId for your primary external tracking identifier (returned as partnerLeadId in responses) and ref for a secondary reference. If you need additional tracking parameters, contact Alleviate engineering.
What happened to est_debt?
The old service accepted est_debt as a partner-supplied estimated debt amount. In MLAS, actual debt amounts are determined automatically through the credit pull pipeline. If you need to communicate the applicant’s estimated debt, use the loanAmount field (requested loan amount in whole dollars) as the closest equivalent.
Do I need to implement polling?
No. createLead now waits for workflow completion and returns the completed result.
If your partner configuration uses manual CRM submission, call submitLead for qualified leads (1013-1017).
Need Help?
If you run into issues during migration, contact the Alleviate engineering team. Be sure to include:
- Your Partner/Client ID
- The environment you’re testing against (Sandbox or Production)
- The full request and response (with sensitive data redacted)
- Any error messages you’re receiving
Next Steps
- Review the full Authentication guide
- Explore the Environments page
- Try the Quick Start for a hands-on walkthrough
- See the complete createLead mutation reference
- Check the Result Codes reference for all status codes