# Submit a Report Watch CSV from an authorized agent

This is the existing report-submission interface, not an autonomous signup or payment flow. A person first signs in with ChatGPT, arranges monitoring access, creates a report with its rules and schedule, and gives the workflow that report's ID and submission token. Public workspaces require confirmed live payment. Owner or invited-pilot admission is separately controlled. A report must be unpaused, within its plan and outside a processing hold.

- Product and setup: [Report Watch](https://reportwatch.mayd-it.com/).
- Machine-readable contract: [OpenAPI JSON](https://reportwatch.mayd-it.com/openapi.json).
- Workflow recipes and scheduling details: [integration reference](https://reportwatch.mayd-it.com/guides/integration-guide.md).
- Importable n8n workflow: [manual setup guide and verification note](https://reportwatch.mayd-it.com/guides/n8n-csv-monitoring) · [workflow JSON](https://reportwatch.mayd-it.com/examples/n8n-report-watch.json). One attempt, no credentials, automatic schedule or downstream action; your own connection still needs setup and acceptance.
- Try rules without a submission: [free browser checker](https://reportwatch.mayd-it.com/tools/csv-checker). This is not an HTTP validation API and does not create checks, deadlines or alerts.

## Scope and credentials

Only `POST https://reportwatch.mayd-it.com/api/ingest/{monitorId}` is described here. A report token can submit only to its own existing monitor. It cannot create accounts or reports, buy a subscription, read arbitrary workspace data or call the owner-only `/mcp` maintenance tools. Do not try private routes or send a person's browser cookies, ChatGPT identity, owner Sites credential, payment key or scheduler secret.

The current token is 64 lowercase hexadecimal characters. A person obtains it when creating a monitor or replacing its submission token; replacement invalidates the old token. Put the token in the execution platform's protected secret store. Do not put a real token in an LLM prompt, source repository, workflow export, debug log or example file. The examples below read a token from an environment variable injected by protected runtime configuration.

Submitting is a write: it can save evidence, create or recover a period's incident and queue a notification. Before enabling a workflow, use synthetic data and verify a pass, an intentional failure, an identical retry and a correction. These examples have not installed or verified your customer's workflow.

## Optional manual n8n workflow

The [n8n workflow JSON](https://reportwatch.mayd-it.com/examples/n8n-report-watch.json) is separate from the [fictional submission-envelope fixture](https://reportwatch.mayd-it.com/examples/report-watch-envelope.json). Import it unpublished, inspect its nodes, replace the deliberately invalid envelope placeholders, and select a protected HTTP Header Auth credential. Follow the [setup guide](https://reportwatch.mayd-it.com/guides/n8n-csv-monitoring), including its current verification note. Make is covered only by the manual configuration recipe in the shared reference.

The workflow validates the saved envelope, makes one raw CSV request and classifies the receipt as `passed`, `quality_failed` or `review`. Its final node performs no downstream action. It does not calculate a schedule occurrence, persist a durable retry queue, retry automatically, create a monitor or buy access. The Node.js example below has a different, explicitly bounded retry policy.

An operator must supply an eligible monitor and keep CSV bytes, deadline and submission ID stable across retries and process restarts. n8n may retain execution inputs and headers or print complete execution data through its CLI; review that platform's access and retention settings. No real credential belongs in a workflow export or envelope, and a synthetic loopback test cannot prove live alerts or your installed customer workflow.

## Freeze one submission envelope

Persist the exact CSV text, monitor ID, submission ID and intended deadline before the first attempt. Reload that same immutable envelope after a process restart. A timeout does not establish whether the first request committed.

| Request field | Contract |
| --- | --- |
| Method | `POST` |
| Authorization | `Bearer <report-token>` with one space after `Bearer` |
| Content-Type | `text/csv; charset=utf-8` |
| Idempotency-Key | Stable, nonblank ID, up to 128 UTF-16 units; use printable ASCII with no surrounding whitespace |
| X-Report-Deadline | Exactly 13 digits: the intended scheduled deadline in Unix milliseconds |
| Body | Raw uncompressed UTF-8 CSV text, not JSON, base64, multipart data or a file URL |

Do not follow redirects or add an `Origin` header. Keep the target origin fixed. The endpoint uses its report token, not browser sign-in.

The deadline must be a real occurrence under the monitor's current schedule, at or after monitor creation and no later than its next upcoming occurrence. Do not add grace to the header. Do not substitute current time or the monitor's advancing next-due value on a retry. A late Monday report still identifies Monday. Local schedules need deliberate timezone and daylight-saving handling; see the integration reference. Do not implement them by blindly adding 24 hours in UTC.

A correction gets a **new submission ID and the same original deadline**. A different period gets its own deadline and ID. Never change the ID just to get around an error. Pausing, changing a schedule, replacing a token or losing monitoring access can make a formerly valid retry fail; eligibility is checked before duplicate lookup. Rule changes do not re-evaluate a saved submission ID.

## One bounded curl request

This shell example uses an existing immutable `report.csv`. The runtime must supply `REPORT_WATCH_TOKEN`, `REPORT_WATCH_MONITOR_ID`, `REPORT_WATCH_DEADLINE_MS` and `REPORT_WATCH_SUBMISSION_ID` from the approved saved submission and secret store. It does not calculate a deadline or create an account. Do not run with shell tracing or curl verbose logging.

```sh
curl --silent --show-error --fail-with-body \
  --proto '=https' --connect-timeout 10 --max-time 30 \
  --request POST \
  --header "Authorization: Bearer ${REPORT_WATCH_TOKEN:?}" \
  --header 'Content-Type: text/csv; charset=utf-8' \
  --header "Idempotency-Key: ${REPORT_WATCH_SUBMISSION_ID:?}" \
  --header "X-Report-Deadline: ${REPORT_WATCH_DEADLINE_MS:?}" \
  --data-binary @report.csv \
  --write-out '\nHTTP_STATUS=%{http_code}\n' \
  "https://reportwatch.mayd-it.com/api/ingest/${REPORT_WATCH_MONITOR_ID:?}"
```

Curl does not follow redirects unless requested; do not add `--location`. This one-shot inspection command prints the JSON body and HTTP status. Its exit code alone is not a quality result: HTTP 200 can carry `status: failed`, and a redirect is not a successful submission. Production callers must validate the response before proceeding.

## Node.js: fixed origin, timeout and bounded retries

This example is for modern **Node.js**, whose built-in fetch supports `redirect: 'error'`. It is not a Cloudflare Worker recipe. Save the following as an `.mjs` file and use a supported Node release. There are no package dependencies.

The existing protected workflow creates `submission-envelope.json` containing `monitorId`, `submissionId`, `deadlineMs` and `csv` **before** this script starts. It must be durable and immutable through retries; no report generation takes place inside the loop. This file contains report data, so give it appropriate local access and retention controls. `REPORT_WATCH_TOKEN` is injected separately; it is never written into the envelope.

```js
import {readFile} from 'node:fs/promises';
import {setTimeout as delay} from 'node:timers/promises';

const envelope = JSON.parse(await readFile('submission-envelope.json', 'utf8'));
const token = process.env.REPORT_WATCH_TOKEN;
if (!/^[a-f0-9]{64}$/.test(token ?? '') ||
    typeof envelope.monitorId !== 'string' ||
    !/^[A-Za-z0-9_-]{1,128}$/.test(envelope.monitorId) ||
    typeof envelope.submissionId !== 'string' ||
    !/^[!-~]{1,128}$/.test(envelope.submissionId) ||
    typeof envelope.deadlineMs !== 'string' || !/^\d{13}$/.test(envelope.deadlineMs) ||
    typeof envelope.csv !== 'string') {
  throw new Error('Review the saved envelope and protected report token.');
}
const bytes = Buffer.from(envelope.csv, 'utf8');
if (bytes.byteLength > 1048576) throw new Error('CSV exceeds 1 MiB.');
const deadline = Number(envelope.deadlineMs);
const url = 'https://reportwatch.mayd-it.com/api/ingest/' + encodeURIComponent(envelope.monitorId);
const headers = {
  Authorization: 'Bearer ' + token,
  'Content-Type': 'text/csv; charset=utf-8',
  'Idempotency-Key': envelope.submissionId,
  'X-Report-Deadline': envelope.deadlineMs,
};

function validReceipt(value) {
  const object = x => x !== null && typeof x === 'object' && !Array.isArray(x);
  const strings = x => Array.isArray(x) && x.every(item => typeof item === 'string');
  return object(value) && typeof value.checkId === 'string' && value.checkId.length > 0 &&
    typeof value.duplicate === 'boolean' && ['passed', 'failed'].includes(value.status) &&
    Number.isInteger(value.rowCount) && value.rowCount >= 0 && value.rowCount <= 10000 &&
    strings(value.columns) && value.columns.length <= 100 && strings(value.issues) &&
    Array.isArray(value.checks) && value.checks.every(check => object(check) &&
      ['key', 'label', 'message'].every(key => typeof check[key] === 'string') &&
      typeof check.passed === 'boolean') &&
    Number.isSafeInteger(value.checkedAt) && value.checkedAt >= 0 &&
    value.deadline === deadline && typeof value.late === 'boolean' &&
    object(value.rules) && strings(value.rules.requiredColumns) &&
    Number.isInteger(value.rules.minRows) && Number.isInteger(value.rules.maxRows) &&
    value.proof === 'Submitted CSV only; independent client delivery is not verified.';
}

async function submit() {
  // Four total attempts; reuse the same bytes, ID and deadline every time.
  const waits = [0, 5000, 30000, 120000];
  for (let attempt = 0; attempt < waits.length; attempt++) {
    if (waits[attempt]) await delay(waits[attempt] + Math.floor(Math.random() * 1000));
    let response;
    try {
      response = await fetch(url, {
        method: 'POST', headers, body: bytes,
        redirect: 'error', credentials: 'omit', signal: AbortSignal.timeout(30000),
      });
    } catch (error) {
      if (error?.name === 'TimeoutError' && attempt < waits.length - 1) continue;
      // Redirects and other network failures stop for review. Keep the envelope:
      // an authorized retry must still use this same submission, never a new ID.
      throw new Error('No verified receipt. Keep the envelope for operator review.');
    }
    if (response.status >= 500 && response.status <= 599) {
      await response.body?.cancel();
      if (attempt < waits.length - 1) continue;
      throw new Error('Temporary service failure persisted; keep the envelope for review.');
    }
    if (response.status !== 200 ||
        !/^application\/json(?:\s*;|$)/i.test(response.headers.get('content-type') ?? '')) {
      await response.body?.cancel();
      throw new Error('Unexpected status or content type; review access/configuration. No automatic retry.');
    }
    let result;
    try { result = await response.json(); }
    catch { throw new Error('No complete JSON receipt; keep the envelope for review.'); }
    if (!validReceipt(result)) throw new Error('Unexpected receipt shape; keep the envelope for review.');
    return result; // Failed quality checks are saved outcomes, not retry triggers.
  }
  throw new Error('Retry budget exhausted; keep the envelope for review.');
}

const result = await submit();
// Store the validated receipt in your protected workflow evidence, not a prompt.
console.log({checkId: result.checkId, status: result.status, late: result.late, duplicate: result.duplicate});
if (result.status === 'failed') process.exitCode = 2; // Route to correction/review.
```

The example checks the fields needed to classify a receipt. For a strict generated client, validate the complete response against `CheckResult` in the OpenAPI document, including nested rules and diagnostics. HTML, redirects, missing fields or a mismatched deadline are not passing results. Do not log CSV, rule/header values, raw responses or tokens. A body-read error after HTTP 200 is also uncertain; preserve the envelope for an authorized identical retry.

## Interpret outcomes separately

| Outcome | Required handling |
| --- | --- |
| `200` with `status: passed` | Store the check receipt; inspect `late`. This proves only the submitted CSV met its rules. |
| `200` with `status: failed` | Store failed evidence and request correction. Do not retry unchanged expecting re-evaluation. |
| `200` with `duplicate: true` | Original saved result, including original `checkedAt`, `rules` and `late`. No new check was evaluated. |
| `400` | Fix missing/invalid ID or deadline. |
| `401`, `402`, `403`, `423` | Stop for credential, entitlement, access or hold review. Do not buy plans or rotate tokens automatically. |
| `404` | Wrong method or route. Use only the documented endpoint. |
| `409` | Review pause/configuration or an ID reused for different content/deadline. Do not bypass with a new ID. |
| `413`, `415` | Fix file size, encoding, compression or content type. |
| Application `429` | Stop automatic retries and inspect quota. Waiting cannot clear the total saved-check cap. No application `Retry-After` is currently supplied. |
| Timeout or `5xx` | Bounded identical retries may be appropriate; retain the envelope after exhaustion. Other network failures in the sample stop for review. |
| Redirect, HTML, unexpected JSON or other status | Stop for routing/configuration review. Do not forward credentials elsewhere. |

Application errors use `{"error":"human-readable explanation"}`. The text is not a stable machine error code. Hosting or proxy errors can have another shape; do not treat them as application receipts. `duplicate` can accompany either a pass or a failure. `late` does not turn a structural pass into a failure and does not make it apply to the next period.

## Limits and operational boundaries

Requests allow **1 MiB of actual bytes**, 100 columns, 10,000 data rows, 100 UTF-16 units per header before and after normalization, and 16,384 UTF-16 units per data cell. A workspace has 1,000 new checks per rolling 24 hours and 5,000 saved checks total. Failed checks count; identical valid retries do not consume another slot. At the total-storage cap, ask support about export and retention. Waiting does not remove saved checks.

Headers are trimmed and lowercased; blank or duplicate normalized headers fail. Optional date checks evaluate every row using the original server check time, not the deadline. ISO date-only values mean midnight UTC; timestamps require a timezone. Dates more than five minutes in the future fail. The current saved-monitor freshness range is 1–8,760 hours; the free browser tool can offer different exploratory settings.

The monitor's stored rules and schedule determine the check. This endpoint does not accept replacement rules in a query or JSON body. Configuration changes need a person in the workspace. A failed CSV that meets the transport requirements is saved with HTTP 200. An empty file or a structural-limit failure can therefore be a quality result; an oversized request body is HTTP 413.

A late pass recovers only its named deadline. Revisions can change saved incident state, but each report deadline can queue only one quality-failure notice and one recovery notice. Missing-report notices are separate. Background processing and provider delivery take time; a queued email is not proof of receipt. Never use Report Watch as the sole check for critical reporting.

Raw CSV rows are processed in memory by the monitoring service. Normalized column names, rules, fingerprints, check evidence and related account/notification records are retained. Your agent runner may store its own input, prompts, files and logs; configure those separately. Use synthetic or sanitized reports and never send personal, confidential or regulated report data merely to try this example.
