Is RoboApply safe?

High risk

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.

RoboApply LLCv2.3.3Chrome Web Store
72Risk
Who publishes it

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.

Publisher
RoboApply LLC
Registered address
2001 Ross Ave, Dallas, TX 75201, US
Registered contact
RoboApply LLC

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.

glassdoor.com.ar
Also called by 2 other listings, including FastApply - Free AI Automated Job Application Copilot
glassdoor.at
Also called by 3 other listings, including FastApply - Free AI Automated Job Application Copilot
glassdoor.com.mx
Also called by 3 other listings, including FastApply - Free AI Automated Job Application Copilot
glassdoor.co.nz
Also called by 5 other listings, including FastApply - Free AI Automated Job Application Copilot
glassdoor.ie
Also called by 5 other listings
glassdoor.com.br
Also called by 6 other listings
glassdoor.sg
Also called by 6 other listings

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
What actually happens
You did this

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 did this

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.

Fields assembled for the Discord report
  • Job page URL
    https://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 text
    Please 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 email
    Jane Applicant > jane.applicant@example.com

    This can identify you as the applicant associated with the job application.

  • Visible tab screenshot
    screenshot.png

    The attachment can show whatever was visible in the browser tab at the time of the form error.

  • Job platform
    workable

    This identifies which supported job site was being used when the report was created.

Captured request
POSThttps://discord.com/api/webhooks/1391348651863048242/<redacted>

The source throws an error if Discord returns a non-OK response.

The code that does this

Validation errors flow from the page script to the Discord webhook sender

Readable version

Readable equivalent of the content-script validation branch

main.6544eb09.js
async 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.js
async 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);  }}
External destination used by the error reporter
    • 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.

Updated 30 September 2026omhfaemlhfikakkigibnfjcpaibpenha