Is RoboApply safe?
RoboApply sends application-form error reports with applicant details and screenshots to Discord webhooks.
When RoboApply detects validation errors or missing required fields while applying on supported job sites, it reports the issue from the content script to the background script. The report sent to Discord includes the job URL and platform, applicant name and email, the related question/error payload, and a visible-tab screenshot when capture succeeds.
Who publishes itRoboApply LLC - no other listings under this identity, 7 shared hostnames
RoboApply LLC - no other listings under this identity, 7 shared hostnames
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 7 hostnames
Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Job Form Error Reports Sent to Discord
The extension sends an error report to Discord when a job application form has a validation error or unfinished fields: job URL, visible error/question text, related questions, applicant name/email, and a screenshot when capture succeeds.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You run the extension on a supported job application and the form reports an error or missing required field.
The source checks error text, required file inputs, and required text fields before continuing the application flow.
The extension builds a Discord error report with page, form, applicant, and screenshot context.
The background worker sends the report to a platform-specific Discord webhook with a screenshot attachment when available.
- Job page URLhttps://jobs.ashbyhq.com/example-company/12345678-90ab-cdef-1234-567890abcdef/application
This links the report to the exact job application page you were using.
- Error and question textPlease enter a valid phone number
This shows which part of the application form needed correction and can include nearby question wording.
- Related form questions[{"label":"phone number","type":"text","required":true,"isRelatedToError":true}]
The report includes the questions the extension marked as related to the form error.
- Applicant name and emailJane Applicant > jane.applicant@example.com
This can identify you as the applicant associated with the job application.
- Visible tab screenshotscreenshot.png
The attachment can show whatever was visible in the browser tab at the time of the form error.
- Job platformworkable
This identifies which supported job site was being used when the report was created.
The source throws an error if Discord returns a non-OK response.
Validation errors flow from the page script to the Discord webhook sender
Readable equivalent of the content-script validation branch
main.6544eb09.jsasync function checkIsReady() { if (this.skipped || window.stoped) return; const errorNodes = document?.querySelectorAll( '.error-text,[class*="errorText"],[class*="_validationError_"],[id*="errorText"],[id*="error-text"], [id$="_error"]' ); const errorNode = Array.from(errorNodes).find((node) => node?.textContent?.trim() !== ''); const errorMessage = errorNode?.textContent?.trim() || ''; const missingRequiredFile = Array.from(document.querySelectorAll('input[type="file"][required]')).some( (input) => !input?.files?.length ); const missingRequiredValue = Array.from( document.querySelectorAll('input[required]:not([type="file"]), textarea[required]') ).some((input) => !input?.value?.trim()); if (errorMessage || missingRequiredFile || missingRequiredValue) { errorNode?.scrollIntoView({ behavior: 'smooth' }); const relatedQuestions = (await this.getQuestions()) .map((question) => { const isRelatedToError = question.parentEl?.contains(errorNode); delete question.inputEl; delete question.parentEl; delete question.id; delete question.section; return { ...question, isRelatedToError }; }) ?.filter((question) => question.isRelatedToError); const content = { tabURL: window.location.href, errorMessage, errorQuestion: errorNode?.parentElement?.parentElement?.textContent?.slice(0, 300), questions: relatedQuestions }; if (!hasSentDiscordReport) { sendMessageToBG({ type: 'sendDiscordMessage', content }); hasSentDiscordReport = true; } } devLog({ errorMessage, missingRequiredFile, missingRequiredValue }); return !(errorMessage?.length > 0) && !missingRequiredFile && !missingRequiredValue;}Readable equivalent of the background Discord sender
static/background/index.jsasync function sendDiscordMessage(errorReport) { let body; let screenshotBlob = null; try { const screenshotDataUrl = await chrome.tabs.captureVisibleTab({ format: 'png' }); screenshotBlob = await (await fetch(screenshotDataUrl)).blob(); } catch (captureError) { console.error('Error capturing screenshot!'); } const queue = await store.getCurrentQueue(); const currentJob = queue?.currentJob; const platform = await store.getPlatform(); const email = queue?.resume?.personalInformation?.email; const fullName = queue?.resume?.personalInformation?.firstName + ' ' + queue?.resume?.personalInformation?.lastName; const previousPayload = await storage.getItem('previousPayload'); if (platform === 'indeed') { const origin = new URL(currentJob.url).origin; currentJob.url = origin + '/viewjob?jk=' + currentJob.id; } const embed = { title: 'Error report: ' + errorReport.errorMessage, color: 14037041, fields: [ { name: 'Job URL', value: currentJob?.url || 'N/A', inline: false }, { name: 'Error QuestionText', value: errorReport.errorQuestion || 'N/A', inline: false }, { name: 'Platform', value: platform || 'N/A', inline: true }, { name: 'User', value: fullName + ' > ' + email, inline: false }, { name: 'Questions', value: '```json\n' + JSON.stringify(errorReport.questions, null, 2).slice(0, 1024) + '\n```', inline: false } ], footer: { text: 'RoboApply Error Reporter' }, timestamp: new Date().toISOString(), ...(screenshotBlob ? { image: { url: 'attachment://screenshot.png' } } : {}) }; const payloadJson = JSON.stringify({ embeds: [embed] }); if (payloadJson === previousPayload) { return console.log('Duplicate Error, Skipping it!!!'); } await storage.setItem('previousPayload', payloadJson); const headers = {}; if (screenshotBlob) { const form = new FormData(); form.append('payload_json', payloadJson); form.append('files[0]', screenshotBlob, 'screenshot.png'); body = form; } else { body = payloadJson; headers['Content-Type'] = 'application/json'; } const webhookUrl = platformWebhookUrls[platform] || defaultWebhookUrl; const response = await fetch(webhookUrl, { method: 'POST', headers, body }); if (!response.ok) { const responseText = await response.text(); throw Error('Discord webhook failed: ' + response.status + ' - ' + responseText); }}- discord.com
Receives the platform-specific webhook POST containing the error-report embed and screenshot attachment when capture succeeds.
Where it sends data
Destinations our analysis observed RoboApply contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- discord.com/api/webhooks
RoboApply sends data to discord.com/api/webhooks. One other extension we have analysed sends data here.