Learn the production-ready best practice of validating requests before dispatching, with complete implementation examples in 4 languages.
Overview
The Validate-then-Send pattern is a critical best practice for production integrations:
Validate the request structure and data (fast, free, safe)
Fix any validation errors
Send the real dispatch once validation passes
This two-step approach prevents errors, saves credits, and ensures data quality before external API calls.
Why Validate First?
Benefits
Benefit
Impact
Catch errors early
Before external API calls, before credits charged
Free validation
No credits consumed for validation
Fast feedback
< 100ms response vs 2-10s for real dispatch
No side effects
No records created in external systems
Safe testing
Perfect for development and testing
Better UX
Show validation errors to users before submission
What Validation Checks
Validation performs comprehensive checks without calling external providers:
โ Request structure and required fields
โ Data types and format
โ Template existence and accessibility
โ Provider configuration presence
โ Business logic (amounts, dates, references)
โ Template data schema compliance
โ Does NOT call external provider APIs
โ Does NOT create real records
โ Does NOT charge credits
Prerequisites
Docs-Dispatcher account with email and password
At least one provider configured
Template created in Templater
Basic understanding of REST APIs
Step 1: Authentication
Dispatcher API uses HTTP Basic Authentication. You'll need your Dispatcher account email and password for all API calls.
Setup Credentials
No token management needed! Unlike JWT-based APIs, Basic Auth doesn't require separate authentication endpoints or token refresh. Your credentials are sent with each request.
# Store credentials as environment variables
export EMAIL="[email protected]"
export PASSWORD="your-password"
POST /api/invoicing/validate
POST /api/email/validate
POST /api/sms/validate
POST /api/postal/validate
POST /api/esign/validate
POST /api/upload/validate
{
"valid": true,
"message": "Request is valid and ready for dispatch",
"warnings": []
}
{
"valid": false,
"errors": [
{
"field": "template.data.customer.email",
"message": "Invalid email format",
"value": "invalid-email",
"expected": "Valid email address"
},
{
"field": "template.data.total",
"message": "Total (1800.00) does not match items sum (1500.00)",
"expected": 1500.00,
"actual": 1800.00
}
]
}
{
"valid": true,
"message": "Request is valid with warnings",
"warnings": [
{
"field": "template.data.dueDate",
"message": "Due date is only 7 days from invoice date (typically 30+ days)",
"level": "warning",
"value": "2026-02-20"
},
{
"field": "template.data.items",
"message": "Large number of line items (45) may affect rendering performance",
"level": "info",
"value": 45
}
]
}
async function validateAndFixErrors(requestData) {
let attempts = 0;
const maxAttempts = 3;
while (attempts < maxAttempts) {
const validation = await validateRequest(requestData);
if (validation.valid) {
console.log('Validation passed!');
if (validation.warnings && validation.warnings.length > 0) {
console.warn('Warnings:', validation.warnings);
}
return requestData;
}
console.error(`Validation failed (attempt ${attempts + 1}):`, validation.errors);
// Fix errors programmatically
requestData = fixValidationErrors(requestData, validation.errors);
attempts++;
}
throw new Error('Could not fix validation errors after ' + maxAttempts + ' attempts');
}
function fixValidationErrors(requestData, errors) {
errors.forEach(error => {
switch (error.field) {
case 'template.data.total':
// Recalculate total
const items = requestData.template.data.items;
const subtotal = items.reduce((sum, item) => sum + item.total, 0);
requestData.template.data.total = subtotal * 1.20; // Add 20% tax
break;
case 'template.data.customer.email':
// Fix email format
requestData.template.data.customer.email =
requestData.template.data.customer.email.toLowerCase().trim();
break;
case 'template.data.dueDate':
// Ensure due date is after invoice date
const invoiceDate = new Date(requestData.template.data.date);
const dueDate = new Date(invoiceDate);
dueDate.setDate(dueDate.getDate() + 30);
requestData.template.data.dueDate = dueDate.toISOString().split('T')[0];
break;
}
});
return requestData;
}
// Show validation errors to users before submission
async function handleFormSubmit(formData) {
const requestData = buildRequestFromForm(formData);
// Validate first
const validation = await validate(requestData);
if (!validation.valid) {
// Show errors to user
displayValidationErrors(validation.errors);
return;
}
// Show warnings to user (optional)
if (validation.warnings && validation.warnings.length > 0) {
const userConfirmed = await confirmWithWarnings(validation.warnings);
if (!userConfirmed) {
return;
}
}
// Proceed with dispatch
const result = await dispatch(requestData);
displaySuccess(result);
}