Troubleshooting common issues
To fix Postman errors with Tallyfy’s API, check the X-Tallyfy-Client header first, verify your authentication grant type is password not client_credentials, and ensure tokens haven’t expired. These three issues cause 90% of API failures.
Postman community experts have identified these patterns from thousands of support cases:
-
Missing X-Tallyfy-Client header (50% of cases)
X-Tallyfy-Client: APIClientEvery single request needs this. No exceptions.
Expert insight: This custom header prevents unauthorized API usage and helps Tallyfy track client types. Unlike standard OAuth implementations, many SaaS APIs use custom headers for additional security layers.
-
Wrong grant type (30% of cases)
- You used:
grant_type=client_credentials
- You need:
grant_type=password
- Client credentials only work for system-level operations
Why this happens: Developers assume
client_credentials
should work everywhere because it’s “cleaner” - but OAuth 2.0 spec correctly separates machine-to-machine vs user-context operations. - You used:
-
Expired token (15% of cases)
- Tokens last exactly 3600 seconds (1 hour)
- Check token age:
pm.environment.get("TALLYFY_TOKEN_EXPIRY")
- Implement auto-refresh (see Authentication Setup guide)
Debugging script:
// Advanced token expiry checkerconst tokenExpiry = pm.environment.get("TALLYFY_TOKEN_EXPIRY");const tokenIssued = pm.environment.get("TALLYFY_TOKEN_ISSUED");const now = Date.now();if (tokenExpiry) {const timeLeft = tokenExpiry - now;const totalLife = tokenExpiry - tokenIssued;const percentUsed = ((totalLife - timeLeft) / totalLife * 100).toFixed(1);console.log(`Token status: ${percentUsed}% used, ${Math.round(timeLeft/1000/60)} minutes remaining`);if (timeLeft < 0) {console.error("Token expired!");} else if (timeLeft < 300000) { // 5 minutesconsole.warn("Token expires soon, consider refreshing");}} -
Bearer token format (5% of cases)
// Wrong formats:Authorization: Bearer[token] // No spaceAuthorization: Bearer [token] // Double spaceAuthorization: [token] // Missing BearerAuthorization: bearer [token] // Lowercase// Correct (exactly one space):Authorization: Bearer [token]Advanced debugging: The Authorization header is case-sensitive and space-sensitive. Many developers copy-paste tokens and accidentally include extra whitespace.
Expert pattern: Support teams often switch between different user accounts and API keys. Here’s how to handle this cleanly:
// Multi-user credential managerconst userProfiles = { "admin": { clientId: pm.vault.get("ADMIN_CLIENT_ID"), clientSecret: pm.vault.get("ADMIN_CLIENT_SECRET"), username: "admin@company.com", context: "full_access" }, "readonly": { clientId: pm.vault.get("READONLY_CLIENT_ID"), clientSecret: pm.vault.get("READONLY_CLIENT_SECRET"), username: "readonly@company.com", context: "read_only" }};
const currentUser = pm.environment.get("TEST_USER_PROFILE") || "admin";const profile = userProfiles[currentUser];
if (profile) { pm.environment.set("TALLYFY_CLIENT_ID", profile.clientId); pm.environment.set("TALLYFY_CLIENT_SECRET", profile.clientSecret); pm.environment.set("TALLYFY_USERNAME", profile.username); console.log(`Switched to ${currentUser} profile (${profile.context})`);} else { console.error(`Unknown user profile: ${currentUser}`);}
Token endpoint is picky about format:
Wrong content type:
// You have:Content-Type: application/json
// You need:Content-Type: application/x-www-form-urlencoded
Wrong body format:
// Wrong - JSON body:{ "grant_type": "password", "username": "user@example.com"}
// Right - URL encoded:grant_type=password&username=user@example.com&password=...
Debug helper script:
// Add to Pre-request Script to log what you're sendingconsole.log("Token Request Details:");console.log("URL:", pm.request.url.toString());console.log("Method:", pm.request.method);console.log("Headers:", pm.request.headers.toObject());console.log("Body mode:", pm.request.body.mode);if (pm.request.body.urlencoded) { console.log("Body params:"); pm.request.body.urlencoded.toObject().forEach(param => { console.log(` ${param.key}: ${param.value}`); });}
“Invalid client” or “Client authentication failed”:
-
Wrong environment selected
- Check:
pm.environment.name
- Verify you’re not using dev credentials against production
- Check:
-
Credentials don’t match
- Client ID and Secret must be from same app
- Organization ID must match the credentials
-
Special characters in secrets
- If secret contains
+
,/
,=
ensure proper encoding - Use environment variables, not hard-coded values
- If secret contains
Wrong organization ID:
// Debug: Log what you're usingconsole.log("Org ID:", pm.environment.get("TALLYFY_ORG_ID"));console.log("Full URL:", pm.request.url.toString());
// Common issue: Using user ID instead of org ID// Wrong: user_123abc// Right: org_456def
Resource was deleted:
// Add error handlingpm.test("Resource exists", function() { if (pm.response.code === 404) { console.error("Resource not found. It may have been deleted."); console.error("URL attempted:", pm.request.url.toString()); }});
API path typo:
// Common typos:// Wrong: /organization/ (singular)// Right: /organizations/ (plural)
// Wrong: /checklist/ (singular)// Right: /checklists/ (plural)
// Wrong: /run/ (singular)// Right: /runs/ (plural)
This means your data format is wrong:
Missing required fields:
// Log what you're sendingconsole.log("Request body:", pm.request.body.raw);
// Common missing fields:// - name (for process launch)// - captures (for task completion)// - field_id (for file uploads)
Wrong data types:
// Common type mismatches:
// Number field expecting number, not string// Wrong: { "field_amount": "100.50" }// Right: { "field_amount": 100.50 }
// Date field expecting specific format// Wrong: { "field_date": "Jan 1, 2024" }// Right: { "field_date": "2024-01-01" }
// Boolean field// Wrong: { "field_approved": "true" }// Right: { "field_approved": true }
Invalid enum values:
// For select/radio fields, value must match exactly// Get valid options first:const template = pm.response.json();const field = template.prerun_fields.find(f => f.id === "field_abc123");console.log("Valid options:", field.options);
Tallyfy’s limits:
- 1000 requests per hour per organization
- Resets on the hour (not rolling window)
Check your current usage:
// Response headers tell you:const remaining = pm.response.headers.get("X-RateLimit-Remaining");const reset = pm.response.headers.get("X-RateLimit-Reset");
console.log(`Requests remaining: ${remaining}`);console.log(`Resets at: ${new Date(reset * 1000).toLocaleTimeString()}`);
Implement backoff strategy:
// Exponential backoff for retriesasync function retryWithBackoff(requestFunction, maxRetries = 3) { let lastError;
for (let i = 0; i < maxRetries; i++) { try { const response = await requestFunction(); if (response.code !== 429) { return response; } lastError = response; } catch (error) { lastError = error; }
// Wait exponentially longer each retry const waitTime = Math.pow(2, i) * 1000; // 1s, 2s, 4s console.log(`Rate limited. Waiting ${waitTime}ms before retry ${i + 1}`); await new Promise(resolve => setTimeout(resolve, waitTime)); }
throw lastError;}
Batch operations efficiently:
// Bad: Rapid fire requeststasks.forEach(task => { pm.sendRequest({...}); // All at once = rate limit});
// Good: Controlled spacingasync function processTasksWithDelay(tasks, delayMs = 100) { for (const task of tasks) { await pm.sendRequest({...}); await new Promise(resolve => setTimeout(resolve, delayMs)); }}
Wrong body type:
// Must use form-data, not raw JSONBody: form-data - file: [File type, select your file] - field_id: [Text type, "field_abc123"]
File too large:
// Check file size before upload// Tallyfy typically limits to 25MBconst maxSize = 25 * 1024 * 1024; // 25MB in bytes
Missing field_id:
// File uploads need to know which field// Find field ID from task data:const task = pm.response.json();const fileField = task.fields.find(f => f.type === 'file');console.log("Use field_id:", fileField.id);
Add this enterprise-grade debugging script to your collection’s Pre-request Script:
// Enterprise-grade debugging for all requests// Inspired by Postman community best practices
const debugLevel = pm.environment.get("DEBUG_LEVEL") || "INFO"; // DEBUG, INFO, WARN, ERRORconst debugLevels = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };const currentLevel = debugLevels[debugLevel] || 1;
function debugLog(level, message, data = null) { if (debugLevels[level] >= currentLevel) { const timestamp = new Date().toISOString(); const prefix = `[${timestamp}] [${level}]`;
if (data) { console.log(prefix, message, JSON.stringify(data, null, 2)); } else { console.log(prefix, message); } }}
debugLog("INFO", "=== REQUEST DEBUG INFO ===");debugLog("DEBUG", "Environment:", pm.environment.name);debugLog("INFO", "Request:", `${pm.request.method} ${pm.request.url.toString()}`);
// Enhanced header validationconst headers = pm.request.headers.toObject();const requiredHeaders = ['Authorization', 'X-Tallyfy-Client'];const optionalHeaders = ['Content-Type', 'Accept', 'User-Agent'];
debugLog("DEBUG", "Header Analysis:");requiredHeaders.forEach(key => { const value = headers[key]; if (value) { const safeValue = key === 'Authorization' ? value.substring(0, 20) + '...' : value; debugLog("DEBUG", ` ✓ ${key}: ${safeValue}`); } else { debugLog("ERROR", ` ✗ ${key}: MISSING! This will cause 401 errors`); }});
optionalHeaders.forEach(key => { const value = headers[key]; if (value) { debugLog("DEBUG", ` ○ ${key}: ${value}`); }});
// Body validation with size checkingif (pm.request.body && pm.request.body.mode) { debugLog("DEBUG", "Body mode:", pm.request.body.mode);
if (pm.request.body.raw) { const bodySize = new Blob([pm.request.body.raw]).size; debugLog("DEBUG", `Body size: ${bodySize} bytes`);
if (bodySize > 1024 * 1024) { // 1MB debugLog("WARN", "Large request body detected, may cause timeouts"); }
try { const parsed = JSON.parse(pm.request.body.raw); debugLog("DEBUG", "Body (parsed):", parsed);
// Validate JSON structure if (pm.request.method === "POST" && pm.request.url.toString().includes("/launch")) { if (!parsed.name) { debugLog("WARN", "Missing 'name' field in launch request"); } } } catch (e) { debugLog("WARN", "Body is not valid JSON:", pm.request.body.raw.substring(0, 200)); } }
if (pm.request.body.formdata) { debugLog("DEBUG", "Form data fields:", pm.request.body.formdata.count()); pm.request.body.formdata.all().forEach(field => { if (field.type === 'file') { debugLog("DEBUG", ` File field: ${field.key}`); } else { debugLog("DEBUG", ` Text field: ${field.key} = ${field.value}`); } }); }}
// Environment variable validationconst criticalVars = { 'TALLYFY_ORG_ID': 'Organization ID for API context', 'TALLYFY_ACCESS_TOKEN': 'Bearer token for authentication', 'TALLYFY_BASE_URL': 'API base URL'};
debugLog("DEBUG", "Environment Variables:");Object.entries(criticalVars).forEach(([key, description]) => { const value = pm.environment.get(key); if (value) { const safeValue = key.includes('TOKEN') ? value.substring(0, 20) + '...' : value; debugLog("DEBUG", ` ✓ ${key}: ${safeValue}`); } else { debugLog("ERROR", ` ✗ ${key}: NOT SET! (${description})`); }});
// Rate limiting awarenessconst lastRequestTime = pm.globals.get("LAST_REQUEST_TIME") || 0;const timeSinceLastRequest = Date.now() - lastRequestTime;if (timeSinceLastRequest < 100) { debugLog("WARN", `Fast consecutive requests (${timeSinceLastRequest}ms apart) - may hit rate limits`);}pm.globals.set("LAST_REQUEST_TIME", Date.now());
// Request sequence trackingconst requestCount = pm.globals.get("REQUEST_COUNT") || 0;pm.globals.set("REQUEST_COUNT", requestCount + 1);debugLog("INFO", `Request #${requestCount + 1} in current session`);
Add this comprehensive response analyzer to your Tests tab:
// Comprehensive response analysis with performance trackingconst debugLevel = pm.environment.get("DEBUG_LEVEL") || "INFO";const responseTime = pm.response.responseTime;const statusCode = pm.response.code;
// Performance analysisconst performanceThresholds = { excellent: 500, good: 1000, acceptable: 2000, poor: 5000};
let performanceRating = "poor";if (responseTime <= performanceThresholds.excellent) performanceRating = "excellent";else if (responseTime <= performanceThresholds.good) performanceRating = "good";else if (responseTime <= performanceThresholds.acceptable) performanceRating = "acceptable";
console.log(`\n=== RESPONSE ANALYSIS (${performanceRating.toUpperCase()}: ${responseTime}ms) ===`);
// Status code analysis with contextconst statusCategories = { 200: "Success", 201: "Created", 400: "Bad Request - Check your request format", 401: "Unauthorized - Authentication failed", 403: "Forbidden - Insufficient permissions", 404: "Not Found - Resource doesn't exist", 422: "Unprocessable Entity - Validation failed", 429: "Too Many Requests - Rate limit exceeded", 500: "Internal Server Error - API issue", 502: "Bad Gateway - API unavailable", 503: "Service Unavailable - Temporary outage"};
const statusContext = statusCategories[statusCode] || "Unknown status";console.log(`Status: ${statusCode} (${statusContext})`);
// Headers analysisconst headers = pm.response.headers.toObject();const importantHeaders = [ 'content-type', 'x-ratelimit-remaining', 'x-ratelimit-reset', 'cache-control', 'etag', 'last-modified'];
console.log("Important Headers:");importantHeaders.forEach(header => { const value = headers[header]; if (value) { console.log(` ${header}: ${value}`); }});
// Rate limiting analysisif (headers['x-ratelimit-remaining']) { const remaining = parseInt(headers['x-ratelimit-remaining']); if (remaining < 100) { console.warn(`⚠️ Rate limit warning: Only ${remaining} requests remaining`); }
// Store for trend analysis const rateLimitHistory = JSON.parse(pm.environment.get("RATE_LIMIT_HISTORY") || '[]'); rateLimitHistory.push({ timestamp: Date.now(), remaining: remaining, endpoint: pm.request.url.getPath() });
// Keep only last 10 entries if (rateLimitHistory.length > 10) { rateLimitHistory.shift(); }
pm.environment.set("RATE_LIMIT_HISTORY", JSON.stringify(rateLimitHistory));}
// Detailed error analysisif (statusCode >= 400) { console.error("\n=== ERROR DETAILS ===");
try { const body = pm.response.json(); console.error("Response body:", JSON.stringify(body, null, 2));
// Tallyfy-specific error formats if (body.error) { console.error(`\n🚨 Error: ${body.error}`); } if (body.message) { console.error(`💬 Message: ${body.message}`); } if (body.errors) { console.error("\n📋 Validation errors:"); Object.entries(body.errors).forEach(([field, errors]) => { const errorList = Array.isArray(errors) ? errors.join(', ') : errors; console.error(` • ${field}: ${errorList}`); }); }
// Common error pattern detection const errorPatterns = { 'Invalid client': 'Check your CLIENT_ID and CLIENT_SECRET', 'Token expired': 'Refresh your access token', 'Missing start boundary': 'Remove manually set Content-Type header for file uploads', 'Validation failed': 'Check required fields and data types', 'Resource not found': 'Verify the resource ID and your permissions' };
const errorText = JSON.stringify(body).toLowerCase(); Object.entries(errorPatterns).forEach(([pattern, solution]) => { if (errorText.includes(pattern.toLowerCase())) { console.error(`\n💡 Suggested fix: ${solution}`); } });
} catch (e) { console.error("Body (text):", pm.response.text()); console.error("Note: Response body is not valid JSON"); }
// Request context for debugging console.error("\n=== REQUEST CONTEXT ==="); console.error(`Method: ${pm.request.method}`); console.error(`URL: ${pm.request.url.toString()}`); console.error(`Environment: ${pm.environment.name}`);
// Add to error log const errorLog = JSON.parse(pm.environment.get("ERROR_LOG") || '[]'); errorLog.push({ timestamp: new Date().toISOString(), status: statusCode, url: pm.request.url.toString(), method: pm.request.method, responseTime: responseTime, environment: pm.environment.name });
// Keep only last 20 errors if (errorLog.length > 20) { errorLog.shift(); }
pm.environment.set("ERROR_LOG", JSON.stringify(errorLog));}
// Success analysisif (statusCode >= 200 && statusCode < 300) { try { const body = pm.response.json();
// Data quality checks if (Array.isArray(body.data)) { console.log(`✅ Retrieved ${body.data.length} items`);
if (body.data.length === 0) { console.warn("⚠️ Empty result set - check your filters"); } }
// Response structure validation const expectedFields = pm.environment.get("EXPECTED_RESPONSE_FIELDS"); if (expectedFields) { const fields = expectedFields.split(','); const missingFields = fields.filter(field => !body.hasOwnProperty(field)); if (missingFields.length > 0) { console.warn(`⚠️ Missing expected fields: ${missingFields.join(', ')}`); } }
} catch (e) { console.log("✅ Request successful (non-JSON response)"); }}
// Store response metricsconst responseMetrics = JSON.parse(pm.environment.get("RESPONSE_METRICS") || '[]');responseMetrics.push({ timestamp: Date.now(), endpoint: pm.request.url.getPath(), method: pm.request.method, status: statusCode, responseTime: responseTime, size: pm.response.text().length});
// Keep only last 50 metricsif (responseMetrics.length > 50) { responseMetrics.shift();}
pm.environment.set("RESPONSE_METRICS", JSON.stringify(responseMetrics));
Postman experts recommend this systematic approach:
-
Enable comprehensive logging:
// Add to collection pre-request scriptpm.globals.set("DEBUG_SESSION_ID", pm.variables.replaceIn('{{$randomUUID}}'));pm.globals.set("DEBUG_START_TIME", new Date().toISOString()); -
Capture complete diagnostic data:
- Enable Postman Console: View → Show Postman Console
- Run the failing request with debug scripts enabled
- Export console log: Save as file
- Export your collection and environment (sanitized)
-
Include these essential details:
- Exact error message and response code
- Request details (method, full endpoint URL)
- Response headers and body
- Your organization ID (for context)
- Timestamp of attempt
- Postman version and operating system
- Debug session ID from step 1
-
Generate a support package:
// Run this in Tests tab to generate support dataconst supportPackage = {sessionId: pm.globals.get("DEBUG_SESSION_ID"),timestamp: new Date().toISOString(),postmanVersion: pm.info.version,environment: pm.environment.name,request: {method: pm.request.method,url: pm.request.url.toString(),headers: pm.request.headers.toObject()},response: {status: pm.response.code,headers: pm.response.headers.toObject(),time: pm.response.responseTime},errorLog: JSON.parse(pm.environment.get("ERROR_LOG") || '[]'),performanceMetrics: JSON.parse(pm.environment.get("RESPONSE_METRICS") || '[]').slice(-5)};console.log("\n=== SUPPORT PACKAGE ===");console.log(JSON.stringify(supportPackage, null, 2));console.log("\nCopy the above JSON and include it with your support request");
Postman Community: Many Tallyfy API issues are common patterns. Search the Postman Community forums for:
- “401 Unauthorized” solutions
- “multipart/form-data” file upload issues
- OAuth authentication troubleshooting
- Environment variable management
Expert tip: Before posting, search for your exact error message in quotes.
Run through this expert checklist before seeking help:
## Postman Diagnostic Checklist
### Authentication- [ ] X-Tallyfy-Client header present and set to "APIClient"- [ ] Authorization header format: "Bearer [token]" (exactly one space)- [ ] Token not expired (check TALLYFY_TOKEN_EXPIRY)- [ ] Using password grant, not client_credentials- [ ] Organization ID correct for your environment
### Request Format- [ ] Method appropriate for operation (POST for create, GET for read, etc.)- [ ] URL path correct and properly encoded- [ ] Content-Type header appropriate for body type- [ ] JSON body valid if using raw mode- [ ] Form-data configured correctly for file uploads (no manual Content-Type)
### Environment- [ ] Correct environment selected- [ ] All required variables set- [ ] No conflicting variables in different scopes- [ ] Network connectivity to go.tallyfy.com
### Response Analysis- [ ] Check response headers for rate limit info- [ ] Verify response body for specific error messages- [ ] Response time within acceptable range (<5 seconds)- [ ] No CORS issues (if testing from browser)
Sometimes issues are at the network level. Use these Postman features:
// Network diagnostics in pre-request scriptconst networkCheck = { startTime: Date.now(), dnsStart: Date.now()};
pm.globals.set("NETWORK_START", JSON.stringify(networkCheck));
// In test script - analyze timingconst networkStart = JSON.parse(pm.globals.get("NETWORK_START"));const totalTime = Date.now() - networkStart.startTime;const responseTime = pm.response.responseTime;const networkTime = totalTime - responseTime;
console.log(`Network timing: ${networkTime}ms network + ${responseTime}ms server = ${totalTime}ms total`);
if (networkTime > 1000) { console.warn("High network latency detected - check connection");}
// Memory usage trackingconst memoryUsage = { environmentVars: Object.keys(pm.environment.toObject()).length, globalVars: Object.keys(pm.globals.toObject()).length, collectionVars: Object.keys(pm.collectionVariables.toObject()).length};
console.log("Memory usage:", memoryUsage);
// Warn about memory leaksif (memoryUsage.environmentVars > 100) { console.warn("High environment variable count - consider cleanup");}
// Performance regression detectionconst currentResponseTime = pm.response.responseTime;const baselineTime = pm.environment.get("BASELINE_RESPONSE_TIME");
if (baselineTime && currentResponseTime > baselineTime * 1.5) { console.warn(`Performance regression: ${currentResponseTime}ms vs baseline ${baselineTime}ms`);}
// Update rolling averageconst responseHistory = JSON.parse(pm.environment.get("RESPONSE_TIME_HISTORY") || '[]');responseHistory.push(currentResponseTime);if (responseHistory.length > 10) { responseHistory.shift();}
const averageTime = responseHistory.reduce((a, b) => a + b, 0) / responseHistory.length;pm.environment.set("RESPONSE_TIME_HISTORY", JSON.stringify(responseHistory));pm.environment.set("AVERAGE_RESPONSE_TIME", averageTime);
// Intelligent issue detectionconst issueDetector = { checkAuthentication() { const authHeader = pm.request.headers.get('Authorization'); const clientHeader = pm.request.headers.get('X-Tallyfy-Client');
const issues = [];
if (!authHeader) { issues.push("Missing Authorization header"); } else if (!authHeader.startsWith('Bearer ')) { issues.push("Invalid Authorization header format"); }
if (!clientHeader) { issues.push("Missing X-Tallyfy-Client header"); } else if (clientHeader !== 'APIClient') { issues.push("Invalid X-Tallyfy-Client header value"); }
return issues; },
checkRequestBody() { const issues = [];
if (pm.request.method === 'POST' && !pm.request.body) { issues.push("POST request without body"); }
if (pm.request.body && pm.request.body.mode === 'raw') { try { JSON.parse(pm.request.body.raw); } catch (e) { issues.push("Invalid JSON in request body"); } }
return issues; },
checkEnvironment() { const issues = []; const requiredVars = ['TALLYFY_BASE_URL', 'TALLYFY_ORG_ID', 'TALLYFY_ACCESS_TOKEN'];
requiredVars.forEach(varName => { if (!pm.environment.get(varName)) { issues.push(`Missing environment variable: ${varName}`); } });
return issues; }};
// Run all checksconst allIssues = [ ...issueDetector.checkAuthentication(), ...issueDetector.checkRequestBody(), ...issueDetector.checkEnvironment()];
if (allIssues.length > 0) { console.error("🚨 Issues detected before sending request:"); allIssues.forEach(issue => console.error(` • ${issue}`));} else { console.log("✅ Pre-flight checks passed");}
The 80/20 rule for Postman debugging:
- 80% of issues: Missing X-Tallyfy-Client header or expired tokens
- 20% of issues: Everything else (data format, permissions, network)
Expert workflow:
- Check X-Tallyfy-Client header first (always)
- Verify token hasn’t expired
- Use Postman Console for detailed request/response inspection
- Implement comprehensive logging scripts
- Use environment variables for sensitive debugging data
- Document common issues and solutions in collection descriptions
Remember: The X-Tallyfy-Client header solves most problems. Check it first, always.
Troubleshooting > Generate HAR files
Support > Contact Tallyfy support
Advanced Patterns > Performance monitoring
- 2025 Tallyfy, Inc.
- Privacy Policy
- Terms of Use
- Report Issue
- Trademarks