* Invoice Generator for Google Apps Script
* Centralized error reporting + full progress receipt next to the generated invoice.
* @author Uncharted Limbo Collective
// ============================================================================
// TYPE DEFINITIONS (unchanged)
// ============================================================================
* Invoice fee entry representing a line item on the invoice.
* @typedef {Object} InvoiceFeeEntry
* @property {string} Description
* @property {number} Quantity
* @property {number} UnitPrice
* @property {number} SubTotal
* @typedef {Object} InvoiceData
* @property {string} ClientLegalName
* @property {string} ClientAddress
* @property {string} [ClientTaxId]
* @property {string} InvoiceReference
* @property {Date} InvoiceDate
* @property {string} InvoiceCurrency
* @property {number} InvoiceDaysNotice
* @property {string} ProjectCode
* @property {number} TotalAmount
* @property {number} NetAmount
* @property {number} VatAmount
* @property {number} VatRate
* @property {string[]} ScopeOfWork
* @property {InvoiceFeeEntry[]} FeeItems
* @typedef {Object} InvoiceCreationResult
* @property {boolean} success
* @property {string} message
* @property {string} [pdfUrl]
* @property {string} [documentUrl]
* @property {Object} [details]
* @property {Object} [details.validationErrors]
* @property {string} [details.filename]
* @property {string} [details.clientName]
* @property {string} [details.projectCode]
* @property {string} [details.currency]
* @property {string} [details.folderPath]
* @property {string} [details.receiptTxtUrl]
* @property {string} [details.receiptJsonUrl]
* @typedef {Object} InvoiceOptions
* @property {boolean} [debugMode=false]
* @property {boolean} [overwriteExisting=false]
* @property {string} [destinationFolderId]
// ============================================================================
// CONFIGURATION (unchanged, fill in your IDs/emails)
// ============================================================================
TEMPLATE_ID_USD: '1vKOJAWj-',
ADMIN_EMAIL: '' // optional: used for serious error notifications
// ============================================================================
// NEW: CENTRALIZED LOGGING + ERROR REPORTING
// ============================================================================
* Progress logger that accumulates a timeline and can write a receipt file.
const ProgressLogger = (() => {
const nowIso = () => new Date().toISOString();
function ProgressLogger(context = {}) {
this.context = context; // e.g. client, project, filename, currency
this.outcome = null; // { success, message }
ProgressLogger.prototype._push = function (level, msg, extra) {
if (extra !== undefined) entry.extra = extra;
this.timeline.push(entry);
// also log to GAS logger for live debugging
Logger.log(`[${level}] ${entry.ts} - ${entry.message}${extra ? ' ' + JSON.stringify(extra) : ''}`);
ProgressLogger.prototype.step = function (msg, extra) { this._push('STEP', msg, extra); };
ProgressLogger.prototype.info = function (msg, extra) { this._push('INFO', msg, extra); };
ProgressLogger.prototype.warn = function (msg, extra) { this._push('WARN', msg, extra); };
ProgressLogger.prototype.error = function (msg, extra) { this._push('ERROR', msg, extra); };
ProgressLogger.prototype.setOutcome = function (success, message, extra) {
this.outcome = { ts: nowIso(), success: !!success, message: String(message) };
if (extra) this.outcome.extra = extra;
ProgressLogger.prototype.toJSON = function () {
ProgressLogger.prototype.toText = function () {
'===== INVOICE GENERATION RECEIPT =====',
`Generated: ${nowIso()}`,
`Client: ${this.context.clientName || '-'}`,
`Project: ${this.context.projectCode || '-'}`,
`InvoiceRef: ${this.context.invoiceReference || '-'}`,
`Filename: ${this.context.filename || '-'}`,
`Currency: ${this.context.currency || '-'}`,
const steps = this.timeline.map(e => `[${e.level}] ${e.ts} :: ${e.message}${e.extra ? ' :: ' + JSON.stringify(e.extra) : ''}`).join('\n');
`Success: ${this.outcome ? this.outcome.success : '-'}`,
`Message: ${this.outcome ? this.outcome.message : '-'}`,
this.outcome && this.outcome.extra ? `Extra: ${JSON.stringify(this.outcome.extra)}` : ''
return `${header}${steps}${outcome}\n=======================================\n`;
* Saves both TXT and JSON receipts in the given folder, using the filename base.
* Returns { txtUrl, jsonUrl }.
ProgressLogger.prototype.saveReceipts = function (folder, filenameBase) {
if (!folder) throw new Error('Receipt folder not available');
const txtBlob = Utilities.newBlob(this.toText(), 'text/plain', `${filenameBase}__receipt.txt`);
const jsonBlob = Utilities.newBlob(this.toJSON(), 'application/json', `${filenameBase}__receipt.json`);
const txtFile = folder.createFile(txtBlob);
const jsonFile = folder.createFile(jsonBlob);
return { txtUrl: txtFile.getUrl(), jsonUrl: jsonFile.getUrl() };
* Central error reporting. Normalizes error object, logs, and optionally emails admin.
function reportError(logger, err, contextMessage, severity = 'error') {
name: (err && err.name) || 'Error',
message: (err && err.message) || String(err),
stack: (err && err.stack) || 'no stack',
logger.error(contextMessage || 'Unhandled error', normalized);
// Optional: email only on critical failures and if ADMIN_EMAIL set
if (INVOICE_CONFIG.ADMIN_EMAIL && severity === 'critical') {
const subject = `Invoice Generator - Critical Error (${logger.context.invoiceReference || 'unknown'})`;
const body = `${contextMessage}\n\n${JSON.stringify(normalized, null, 2)}\n\nContext:\n${JSON.stringify(logger.context, null, 2)}`;
// MailApp.sendEmail(INVOICE_CONFIG.ADMIN_EMAIL, subject, body);
logger.info('Admin notification prepared (disabled by default).');
logger.warn('Failed to send admin email', { emailErr: String(emailErr) });
// ============================================================================
// HELPER FUNCTIONS (mostly unchanged; minor tweaks to add logging where helpful)
// ============================================================================
function getCurrencySymbol(acronym){
"USD": "$","EUR": "€","JPY": "¥","GBP": "£","AUD": "A$","CAD": "C$","CHF": "Fr.","CNY": "¥",
"INR": "₹","MXN": "Mex$","BRL": "R$","ZAR": "R","RUB": "₽","SGD": "S$","HKD": "HK$","NZD": "NZ$",
"KRW": "₩","SEK": "kr","NOK": "kr","AED": "د.إ","QAR": "﷼",
return currencyDict[acronym];
function formatInvoiceDate(date) {
return Utilities.formatDate(date, Session.getScriptTimeZone(), 'dd/MM/yyyy');
function getTemplateIdForCurrency(currency) {
'EUR': INVOICE_CONFIG.TEMPLATE_ID_EUR,
'USD': INVOICE_CONFIG.TEMPLATE_ID_USD,
'GBP': INVOICE_CONFIG.TEMPLATE_ID_GBP
return templates[currency] || INVOICE_CONFIG.TEMPLATE_ID_GBP;
function generateInvoiceFilename(invoiceDate, invoiceReference) {
const dateStr = invoiceDate.toISOString().slice(2, 10).replace(/-/g, '');
return `${dateStr}_${invoiceReference}`;
function findFileInFolder(folder, filename) {
const files = folder.getFilesByName(filename);
return files.hasNext() ? files.next() : null;
// ============================================================================
// VALIDATION (unchanged except removed stray Logger warning)
// ============================================================================
function validateInvoiceData(data) {
if (!data.ClientLegalName || data.ClientLegalName.trim() === '') {
errors.ClientLegalName = 'Client legal name is required';
if (!data.ClientAddress || data.ClientAddress.trim() === '') {
// optional: allow blank address but note it
errors.ClientAddress = 'Client address is required';
if (!data.InvoiceReference || data.InvoiceReference.trim() === '') {
errors.InvoiceReference = 'Invoice reference is required';
if (!(data.InvoiceDate instanceof Date) || isNaN(data.InvoiceDate)) {
errors.InvoiceDate = 'Valid invoice date is required';
if (!data.InvoiceCurrency || data.InvoiceCurrency.trim() === '') {
errors.InvoiceCurrency = 'Invoice currency is required';
if (typeof data.InvoiceDaysNotice !== 'number' || data.InvoiceDaysNotice < 0) {
errors.InvoiceDaysNotice = 'Valid days notice is required';
if (typeof data.TotalAmount !== 'number' || data.TotalAmount <= 0) {
errors.TotalAmount = 'Valid total amount is required';
if (typeof data.NetAmount !== 'number' || data.NetAmount <= 0) {
errors.NetAmount = 'Valid net amount is required';
if (typeof data.VatAmount !== 'number' || data.VatAmount < 0) {
errors.VatAmount = 'Valid VAT amount is required';
if (typeof data.VatRate !== 'number' || data.VatRate < 0) {
errors.VatRate = 'Valid VAT rate is required';
if (!Array.isArray(data.ScopeOfWork) || data.ScopeOfWork.length === 0) {
errors.ScopeOfWork = 'At least one scope of work item is required';
if (!Array.isArray(data.FeeItems) || data.FeeItems.length === 0) {
errors.FeeItems = 'At least one fee item is required';
isValid: Object.keys(errors).length === 0,
// ============================================================================
// FOLDER MANAGEMENT (unchanged)
// ============================================================================
function getOrCreateInvoiceFolders(invoiceRootFolder, clientName, projectCode, debugMode = false) {
clientFolder: { getName: () => clientName, getId: () => 'debug-client-folder-id', getUrl: () => 'about:blank' },
projectFolder: { getName: () => projectCode, getId: () => 'debug-project-folder-id', getUrl: () => 'about:blank', createFile: () => ({ getUrl: ()=>'about:blank'}) }
const existingClientFolders = [];
const clientFolders = invoiceRootFolder.getFolders();
while (clientFolders.hasNext()) existingClientFolders.push(clientFolders.next());
let clientFolder = existingClientFolders.find(folder => folder.getName() === clientName);
if (!clientFolder) clientFolder = invoiceRootFolder.createFolder(clientName);
const existingProjectFolders = clientFolder.getFolders();
let projectFolder = null;
while (existingProjectFolders.hasNext()) {
const folder = existingProjectFolders.next();
if (folder.getName() === projectCode) { projectFolder = folder; break; }
if (!projectFolder) projectFolder = clientFolder.createFolder(projectCode);
return { clientFolder, projectFolder };
// ============================================================================
// DOCUMENT POPULATION (unchanged functionality)
// ============================================================================
function convertDocumentToPdf(doc, folderId) {
const pdfBlob = doc.getAs('application/pdf');
pdfBlob.setName(doc.getName() + '.pdf');
const pdfFile = DriveApp.createFile(pdfBlob);
const fileId = pdfFile.getId();
const file = DriveApp.getFileById(fileId);
const sourceFolder = DriveApp.getFileById(doc.getId()).getParents().next();
const destinationFolder = DriveApp.getFolderById(folderId);
destinationFolder.addFile(file);
sourceFolder.removeFile(file);
function populateDocumentTemplate(doc, data, currencySymbol) {
const body = doc.getBody();
body.replaceText('{{Client}}', data.ClientLegalName);
body.replaceText('{{Address}}', data.ClientAddress);
body.replaceText('{{Tax_ID}}', data.ClientTaxId || 'N/A');
body.replaceText('{{Invoice Reference}}', data.InvoiceReference);
body.replaceText('{{Date Invoiced}}', Utilities.formatDate(data.InvoiceDate, Session.getScriptTimeZone(), 'dd/MM/yyyy'));
body.replaceText('{{Notice}}', String(data.InvoiceDaysNotice));
body.replaceText('{{Currency}}', data.InvoiceCurrency);
body.replaceText('{{TotalAmount}}', currencySymbol + data.TotalAmount.toFixed(2));
body.replaceText('{{Amount}}', currencySymbol + data.NetAmount.toFixed(2));
body.replaceText('{{VAT}}', currencySymbol + data.VatAmount.toFixed(2));
body.replaceText('{{VAT_Rate}}', (data.VatRate * 100).toFixed(1) + '%');
insertBulletListAfterParagraph(doc, 'Scope of Work', data.ScopeOfWork);
populateInvoiceFeeTable(doc, data.FeeItems, currencySymbol);
function insertBulletListAfterParagraph(doc, parentText, bulletItems) {
if (!doc || typeof doc.getBody !== 'function') throw new Error('Invalid document object provided');
if (!Array.isArray(bulletItems) || bulletItems.length === 0) throw new Error('bulletItems must be a non-empty array');
if (typeof parentText !== 'string' || parentText.trim() === '') throw new Error('parentText must be a non-empty string');
const body = doc.getBody();
const paragraphs = body.getParagraphs();
[DocumentApp.Attribute.FONT_SIZE]: 12,
[DocumentApp.Attribute.FOREGROUND_COLOR]: '#000000'
for (let i = 0; i < paragraphs.length; i++) {
const paragraphText = paragraphs[i].getText();
if (paragraphText === parentText) {
bulletItems.forEach((bulletText, index) => {
const insertPosition = i + 1 + index;
const listItem = body.insertListItem(insertPosition, bulletText);
listItem.setGlyphType(DocumentApp.GlyphType.BULLET);
listItem.setAttributes(bulletStyle);
Logger.log(`Warning: Parent paragraph "${parentText}" not found in document`);
function populateInvoiceFeeTable(doc, feeRows, currencySymbol) {
if (!doc || typeof doc.getBody !== 'function') throw new Error('Invalid document object provided');
if (!Array.isArray(feeRows) || feeRows.length === 0) throw new Error('feeRows must be a non-empty array');
if (typeof currencySymbol !== 'string') throw new Error('currencySymbol must be a string');
const body = doc.getBody();
const tables = body.getTables();
if (tables.length === 0) throw new Error('Document must contain at least one table');
const numRows = feeRows.length;
const centerAlignStyle = { [DocumentApp.Attribute.HORIZONTAL_ALIGNMENT]: DocumentApp.HorizontalAlignment.CENTER };
feeRows.forEach((feeEntry, index) => {
const newRow = table.insertTableRow(1 + index);
newRow.appendTableCell(String(feeEntry.Description)),
newRow.appendTableCell(String(feeEntry.Quantity)),
newRow.appendTableCell(currencySymbol + String(feeEntry.UnitPrice)),
newRow.appendTableCell(currencySymbol + String(feeEntry.SubTotal))
cells.forEach(cell => cell.getChild(0).asParagraph().setAttributes(centerAlignStyle));
applyTableBorderStyling(doc, table, numRows, 4);
function applyTableBorderStyling(doc, table, rowCount, columnCount) {
const docId = doc.getId();
const body = doc.getBody();
const tableIndex = body.getChildIndex(table);
const startIndex = Docs.Documents.get(docId).body.content[tableIndex + 1].startIndex;
const borderStyleRequest = {
borderBottom: { dashStyle: 'SOLID', width: { magnitude: 1, unit: 'PT' }, color: { color: { rgbColor: { red: 0 } } } },
borderRight: { dashStyle: 'SOLID', width: { magnitude: 0, unit: 'PT' }, color: { color: {} } },
borderLeft: { dashStyle: 'SOLID', width: { magnitude: 0, unit: 'PT' }, color: { color: {} } }
tableCellLocation: { tableStartLocation: { index: startIndex }, rowIndex: 1 },
fields: 'borderBottom,borderRight,borderLeft'
Docs.Documents.batchUpdate({ requests: [borderStyleRequest] }, docId);
function notifyDuplicateFile(clientName, filename, folderUrl) {
const subject = `Duplicate Invoice File: ${filename}`;
const body = `An invoice document with the same name already exists.\n\nClient: ${clientName}\nFilename: ${filename}\nFolder: ${folderUrl}\n\nPlease review and take appropriate action.`;
// MailApp.sendEmail(INVOICE_CONFIG.ADMIN_EMAIL, subject, body);
Logger.log(`Duplicate file notification sent: ${filename} - ${folderUrl} - ${clientName} `);
// ============================================================================
// DOCUMENT RENDERING (CHANGED: accepts logger; richer steps/errors)
// ============================================================================
function renderInvoiceDocument(data, docId, options = {}, logger) {
const debugMode = options.debugMode || false;
const localLog = logger || new ProgressLogger({ docId });
localLog.step('Render: validate data');
const validation = validateInvoiceData(data);
if (!validation.isValid) {
localLog.setOutcome(false, 'Validation failed', { errors: validation.errors });
message: 'Validation failed',
validationErrors: validation.errors
localLog.info('Render: debug mode - skipping file ops');
localLog.setOutcome(true, 'Debug OK');
message: 'Debug mode: All validations passed. Document would be created successfully.',
currency: data.InvoiceCurrency,
totalAmount: data.TotalAmount,
feeItemCount: data.FeeItems.length,
scopeItemCount: data.ScopeOfWork.length,
clientName: data.ClientLegalName,
invoiceReference: data.InvoiceReference
if (!docId || typeof docId !== 'string') {
throw new Error('Valid document ID is required');
localLog.step('Render: open template copy');
const doc = DocumentApp.openById(docId);
const currencySymbol = getCurrencySymbol(data.InvoiceCurrency) || '';
localLog.step('Render: populate template placeholders and tables');
populateDocumentTemplate(doc, data, currencySymbol);
localLog.info('Render: population complete');
localLog.step('Render: convert to PDF');
const folderId = options.destinationFolderId ||
DriveApp.getFileById(docId).getParents().next().getId();
const pdfUrl = convertDocumentToPdf(doc, folderId);
localLog.info('Render: PDF created', { pdfUrl });
localLog.setOutcome(true, 'Invoice document created successfully', { pdfUrl });
return { success: true, message: 'Invoice document created successfully', pdfUrl };
const norm = reportError(localLog, error, 'Error rendering invoice document');
localLog.setOutcome(false, 'Error creating invoice document', { error: norm });
message: 'Error creating invoice document: ' + error.message,
validationErrors: { system: String(error) }
// Debug wrapper (unchanged interface)
function debugRenderInvoiceDocument(data, docId) {
return renderInvoiceDocument(data, docId, { debugMode: true });
// ============================================================================
// INVOICE CREATION (CHANGED: centralized logging + receipt saving)
// ============================================================================
function createInvoice(invoiceData, options = {}) {
const debugMode = options.debugMode || false;
const overwriteExisting = options.overwriteExisting || false;
// create a logger as early as possible with context (updated as we learn more)
clientName: invoiceData && invoiceData.ClientLegalName,
projectCode: invoiceData && invoiceData.ProjectCode,
currency: invoiceData && invoiceData.InvoiceCurrency,
invoiceReference: invoiceData && invoiceData.InvoiceReference,
const logger = new ProgressLogger(ctx);
let projectFolder = null;
logger.step('Validate invoice data');
const validation = validateInvoiceData(invoiceData);
if (!validation.isValid) {
logger.setOutcome(false, 'Validation failed', validation.errors);
// No folder to save in, but still return detailed errors
message: `Validation failed: ${Object.keys(validation.errors).map(k => `${k}: ${validation.errors[k]}`).join(', ')}`,
details: { validationErrors: validation.errors }
const clientName = invoiceData.ClientLegalName;
const projectCode = invoiceData.ProjectCode;
const invoiceDate = invoiceData.InvoiceDate;
const invoiceReference = invoiceData.InvoiceReference;
const currency = invoiceData.InvoiceCurrency;
filename = generateInvoiceFilename(invoiceDate, invoiceReference);
// update context with filename
logger.context.filename = filename;
logger.step('Select template based on currency');
const templateId = getTemplateIdForCurrency(currency);
logger.info('Debug mode: simulate folder/document/pdf creation');
logger.setOutcome(true, 'Debug mode: Invoice would be created successfully.');
message: 'Debug mode: All validations passed. Invoice ready for creation.',
invoiceDate: invoiceDate.toISOString(),
wouldCreateFolders: true,
wouldCreateDocument: true,
logger.step('Get or create folder structure');
const invoiceRootFolder = DriveApp.getFolderById(INVOICE_CONFIG.INVOICE_FOLDER_ID);
const folders = getOrCreateInvoiceFolders(invoiceRootFolder, clientName, projectCode, false);
clientFolder = folders.clientFolder;
projectFolder = folders.projectFolder;
logger.info('Folders ready', {
clientFolder: clientFolder.getName(),
projectFolder: projectFolder.getName()
logger.step('Check for existing file collision');
const existingFile = findFileInFolder(projectFolder, filename);
if (existingFile && !overwriteExisting) {
logger.warn('Duplicate file exists; notifying admin and aborting', { filename });
notifyDuplicateFile(clientName, filename, projectFolder.getUrl());
// Save receipt before returning (duplicate case)
const receipts = logger.saveReceipts(projectFolder, filename);
logger.info('Receipt saved for duplicate case', receipts);
logger.setOutcome(false, 'Duplicate file exists. Admin notified.');
message: 'Duplicate file exists. Admin notified.',
folderUrl: projectFolder.getUrl(),
action: 'notification_sent',
receiptTxtUrl: receipts.txtUrl,
receiptJsonUrl: receipts.jsonUrl
logger.step('Create document from template');
const template = DriveApp.getFileById(templateId);
const documentCopy = template.makeCopy(filename, projectFolder);
logger.info('Document copy created', { documentId: documentCopy.getId() });
logger.step('Render invoice (populate + PDF)');
const renderResult = renderInvoiceDocument(invoiceData, documentCopy.getId(), { destinationFolderId: projectFolder.getId() }, logger);
if (!renderResult.success) {
logger.warn('Render failed', { renderMessage: renderResult.message });
const receipts = logger.saveReceipts(projectFolder, filename);
logger.setOutcome(false, 'Failed to render invoice document', { documentId: documentCopy.getId() });
message: 'Failed to render invoice document',
renderError: renderResult.message,
documentId: documentCopy.getId(),
receiptTxtUrl: receipts.txtUrl,
receiptJsonUrl: receipts.jsonUrl
logger.step('Finalize invoice creation');
const pdfUrl = renderResult.pdfUrl;
const docUrl = documentCopy.getUrl();
logger.info('Invoice created', { pdfUrl, docUrl });
const receipts = logger.saveReceipts(projectFolder, filename);
logger.info('Receipt saved', receipts);
logger.setOutcome(true, 'Invoice created successfully');
message: 'Invoice created successfully',
folderPath: `${clientFolder.getName()}/${projectFolder.getName()}`,
receiptTxtUrl: receipts.txtUrl,
receiptJsonUrl: receipts.jsonUrl
const norm = reportError(logger, error, 'Error creating invoice', 'critical');
// If we have a project folder and filename, save a receipt there even for failures.
if (projectFolder && filename) {
receiptUrls = logger.saveReceipts(projectFolder, filename);
logger.info('Receipt saved for failure case', receiptUrls);
logger.warn('Failed to save receipt after error', { saveErr: String(saveErr) });
logger.setOutcome(false, 'Error creating invoice', { error: norm });
message: `Error creating invoice: ${error.message}`,
details: Object.assign({ error: String(error) }, receiptUrls)
// Debug wrapper (unchanged interface)
function debugCreateInvoice(invoiceData) {
return createInvoice(invoiceData, { debugMode: true });
// ============================================================================
// WEB APP ENTRY POINTS (CHANGED: use centralized logging + better messages)
// ============================================================================
const logger = new ProgressLogger({ entryPoint: 'doPost' });
let parsedPayload = null;
logger.step('Auth: validate API key');
const apiKey = e.parameter.apiKey;
if (!apiKey || !Object.values(API_KEYS).includes(apiKey)) {
logger.setOutcome(false, 'Unauthorized: Invalid or missing API key');
.createTextOutput(JSON.stringify({ success: false, message: 'Unauthorized: Invalid or missing API key' }))
.setMimeType(ContentService.MimeType.JSON);
logger.step('Parse request JSON');
parsedPayload = JSON.parse(e.postData.contents);
// Accept both "invoiceData" and "InvoicePayload" for compatibility
const invoiceData = parsedPayload.invoiceData || parsedPayload.InvoicePayload;
const options = parsedPayload.options || {};
logger.info('Normalize date fields');
if (invoiceData.InvoiceDate && typeof invoiceData.InvoiceDate === 'string') {
invoiceData.InvoiceDate = new Date(invoiceData.InvoiceDate);
logger.step('Create invoice');
const result = createInvoice(invoiceData, options);
logger.setOutcome(true, 'Handled doPost successfully');
.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
const norm = reportError(logger, error, 'doPost handler failure', 'critical');
logger.setOutcome(false, 'doPost failed', { error: norm, payloadEchoed: !!parsedPayload });
// Return error response (without Drive receipt because we might not know folder/filename here)
message: 'Error processing request: ' + error.message,
.createTextOutput(JSON.stringify(errorResponse))
.setMimeType(ContentService.MimeType.JSON);
message: 'Invoice Generator API is running',
endpoints: { POST: 'Send invoice data as JSON to create invoice', GET: 'API status check' },
debugMode: e && e.parameter && e.parameter.debug === 'true'
.createTextOutput(JSON.stringify(response))
.setMimeType(ContentService.MimeType.JSON);