# Cross-check integrations: n8n and Make

This importable workflow takes a case from your server, submits its files to the live DeepfakePolicy Cross-check API and produces a structured result with your original `externalCaseId`. You configure the final destination. Importing this file does not connect a CRM, activate the workflow or run a paid check.

Download: https://deepfakepolicy.com/api-examples/n8n-cross-check-workflow.json

## Configure once

1. Import the JSON file into your maintained n8n installation. It is inactive by default. The recipe uses built-in nodes only: Webhook 2.1, Code 2, IF 2.2, HTTP Request 4.2, Respond to Webhook 1.4 and Wait 1.1. Your installed version must support these node versions. The recipe has been checked against the official node definitions and exercised through its exported graph with a mock API; it has not been executed in your n8n instance.
2. In **Incoming case**, select **Header Auth** and create a credential with header name `X-Workflow-Key` and a new secret for your sending server. This is separate from the DeepfakePolicy API key. Keep the webhook on HTTPS. Give each company its own workflow and API credential; do not expose it directly to public customers.
3. Create a second **Header Auth** credential: name `Authorization`, value `Bearer YOUR_LIVE_COMPANY_KEY`. Select it in both **Submit Cross-check** and **Read existing job**. The key needs `jobs:create` and `jobs:read`. Put secrets only in **n8n Credentials**, never in Code nodes, request files, headers configured as plain parameters or exported JSON.
4. In DeepfakePolicy, connect company billing and set the monthly spending limit before processing real files. Completed comparisons, including results with insufficient evidence, are billable. A processing failure is not billed; `credit_refund_pending` means the release is still being reconciled. Sandbox media fixtures do not perform Cross-check comparisons.
5. The optional **Write result to your system — configure then enable** node is disabled. Set a fixed HTTPS destination and a separate destination credential, or replace it with the node for your CRM. Map `externalCaseId` to the existing case, save `jobId`, and map `comparison.findings`, `comparison.counts` and `status` to your chosen fields. Test that mapping before enabling delivery. Do not use an incoming request to choose the destination URL.
6. Test with your own non-sensitive material, then activate the workflow. Use the Production URL displayed by **Incoming case**. A typical path is `/webhook/deepfakepolicy-cross-check-v1`; the instance hostname and base path are yours. The test URL works only while the editor is listening.

The result is a consistency review, not an authenticity verdict or an automatic decision to accept a claim, pay an invoice or approve a return. Keep the review step in your own process.

## Submit a case

Send JSON to your n8n webhook, authenticated with `X-Workflow-Key`.

| Field | Meaning |
| --- | --- |
| `action` | `submit`, `status` or `resume`. |
| `externalCaseId` | Your case-system identifier, 1–160 characters. Keep its mapping to `jobId` in your system. |
| `submissionId` | Required for `submit`: 8–96 letters, digits, `_` or `-`. Generate once per intended analysis and persist it before sending. It becomes the API `Idempotency-Key` with the prefix `n8n-cross-check:`. |
| `comparison` | The complete Cross-check request, for `submit` only. |
| `jobId` | Required for `status`/`resume`: the ID from the first accepted response. These actions reject files and never submit a new analysis. |

Example request shape; replace the base64 placeholders with real file bytes before submission:

```json
{
  "action": "submit",
  "externalCaseId": "RETURN-104",
  "submissionId": "return_104_revision_1",
  "comparison": {
    "templateId": "returns_warranty",
    "templateVersion": 1,
    "locale": "en",
    "description": "Compare the camera serial number and model with the receipt.",
    "files": [
      {"name": "camera.jpg", "contentType": "image/jpeg", "base64": "BASE64_JPEG_BYTES"},
      {"name": "receipt.pdf", "contentType": "application/pdf", "base64": "BASE64_PDF_BYTES"}
    ]
  }
}
```

This Python 3 command prepares that request from two local files without making a network request or starting a check:

```bash
python3 - <<'PY'
import base64, json
from pathlib import Path
files = [("camera.jpg", "image/jpeg"), ("receipt.pdf", "application/pdf")]
request = {
    "action": "submit",
    "externalCaseId": "RETURN-104",
    "submissionId": "return_104_revision_1",
    "comparison": {
        "templateId": "returns_warranty", "templateVersion": 1, "locale": "en",
        "description": "Compare the camera serial number and model with the receipt.",
        "files": [{"name": name, "contentType": mime,
                   "base64": base64.b64encode(Path(name).read_bytes()).decode("ascii")}
                  for name, mime in files]
    }
}
Path("case-request.json").write_text(json.dumps(request), encoding="utf-8")
PY
```

After you configure and activate n8n, this separate command sends the request. A new `submit` can start a billable comparison. Store `N8N_WEBHOOK_URL` and the inbound `N8N_WEBHOOK_KEY` in your sending server's secret environment; the DeepfakePolicy key remains in n8n Credentials.

```bash
python3 - <<'PY'
import os, urllib.request
from pathlib import Path
request = urllib.request.Request(
    os.environ["N8N_WEBHOOK_URL"],
    data=Path("case-request.json").read_bytes(),
    headers={"Content-Type": "application/json", "X-Workflow-Key": os.environ["N8N_WEBHOOK_KEY"]},
    method="POST"
)
with urllib.request.urlopen(request, timeout=100) as response:
    print(response.status)
    print(response.read().decode("utf-8"))
PY
```

One to six JPEG/PNG/WebP/PDF/CSV/XLSX files are accepted, up to 5 MiB each and 10 MiB decoded per case. Use canonical base64, not a data URL or remote file URL. The API validates actual file types, fifty-page PDF limits and bounded spreadsheet parsing. Export spreadsheet formulas as values. The workflow accepts `useCase`, `locale`, `description`, `checklist`, `files`, `caseId`, `templateId`, `templateVersion`, `documentRoles`, `previousJobId` and `reuseExtractions`; the API remains authoritative for template/access validation.

Built-in templates are `returns_warranty`, `equipment_rental`, `contract_documents` and `invoice_payments`, version `1`. Contract checks require exactly one file assigned `primary_contract`, for example `"documentRoles":{"s1":"primary_contract","s2":"company_record"}`. Source IDs follow the `files` order. Optional `caseId` links an already-created, open DeepfakePolicy company case; it is distinct from your external case identifier. Company templates use their saved ID/version. For an intentional follow-up analysis, `previousJobId` can reference a completed, unexpired comparison in the same company case and requires that existing `caseId`; optional `reuseExtractions: true` requests reuse where supported and validated by the server. Without explicit `true`, the server reads the sources again and still compares the retained findings. Send the complete required bundle with a **new** submission ID. This is a new billable analysis, unlike `resume`, which only reads the existing result.

## Request/response and result delivery

The webhook returns `202` after the API accepts a pending job. Save `jobId` against the original case immediately. If the saved job is already complete, an identical retry can return `200` with the result.

Illustrative `202` response:

```json
{
  "schemaVersion": "deepfakepolicy.integration.v1",
  "externalCaseId": "RETURN-104",
  "submissionId": "return_104_revision_1",
  "jobId": "job_709d4392-25e5-4e5d-9ad0-0a01bb2d1717",
  "status": "queued",
  "deliveryKey": "dfp-cross-check:job_709d4392-25e5-4e5d-9ad0-0a01bb2d1717:queued",
  "jobStatus": "queued",
  "retentionExpiresAt": "2026-09-26T12:00:00.000Z",
  "resume": {"action":"resume","externalCaseId":"RETURN-104","jobId":"job_709d4392-25e5-4e5d-9ad0-0a01bb2d1717"},
  "statusRequest": {"action":"status","externalCaseId":"RETURN-104","jobId":"job_709d4392-25e5-4e5d-9ad0-0a01bb2d1717"}
}
```

n8n continues in the background after that response. The final **Result for your system** item contains the original `externalCaseId`, `jobId`, `status`, `deliveryKey` and, when completed, the saved `comparison` including findings, citations, counts and limitations. `reportUrl` is a protected API URL; it requires the company API credential and is not a public share link. To attach a PDF, add an authenticated GET node for `/v1/jobs/{jobId}/report?format=pdf` before your CRM attachment node. Reading or exporting the saved report does not start a new analysis.

`status` returns the current result to the caller and does not send to the configured destination. Submit the returned `statusRequest` JSON to the same authenticated n8n webhook. For a pending job it returns `202`; a completed or failed job returns `200` with its actual `status`. A processing failure never appears as a successful comparison.

`resume` reads the same job, polls if it is still running and sends the final item to the configured destination again. It never performs `POST /v1/comparisons` and does not need original files:

```json
{"action":"resume","externalCaseId":"RETURN-104","jobId":"job_709d4392-25e5-4e5d-9ad0-0a01bb2d1717"}
```

Your receiver should **upsert** by `jobId` within the original `externalCaseId` and deduplicate each `deliveryKey`. Delivery keys include the status, so a later completed result is not suppressed by an earlier timeout notice. Check that `jobId` belongs to the stored case mapping before writing to that case. The external identifier is supplied by your authenticated server, not independently verified by DeepfakePolicy. Give that server access only to the relevant company's workflow.

## Retries and recovery

- A timeout or lost response during submission must be retried with the **same submissionId, identical files/parameters and same API key**. The workflow retries transient requests up to three attempts. DeepfakePolicy binds idempotency to the key; rotating to a different key and resubmitting can create a new check. After the first accepted response, prefer `status`/`resume` with the saved `jobId`.
- A changed request with the original idempotency key returns `409`; it does not overwrite the old analysis. Use a new submission ID only for an intentionally new, billable analysis. Submission IDs must be unique across all cases using the same company key.
- HTTP `429` respects `Retry-After`. If the wait would exceed the recipe's request budget, the workflow returns `retryAfterSeconds` for your caller to schedule. Do not retry earlier. HTTP/network errors get at most three attempts per operation, without generating a new key.
- Polling normally waits ten seconds, ends after sixty successful status reads or a fifteen-minute window, and returns `polling_timeout`. Network time can add up to one in-flight request. A timeout does not cancel the job or prove processing failed. Resume the existing job.
- `status` performs one read per invocation. Unknown job states, invalid responses, terminal errors, deleted reports and expired reports stop the workflow. No error path starts a replacement analysis.
- `upload_required` means an interrupted source upload left the existing job awaiting files. Reads and `resume` cannot complete it. Retry `submit` with the **original submissionId, identical files/parameters and same API key**; the existing idempotent job is reused. The workflow stops polling this state and never invents a replacement submission ID.
- If final delivery fails, use `resume` or retry only the destination node with the saved result. Do not rerun submission under a new key. The optional destination uses up to three retries; receiver-side idempotency is required.
- Results expire seven days after submission. Expired/deleted results are not recreated automatically. Save the result to your approved destination before expiry if your own retention policy permits it.

The webhook does not expose the n8n execution history as a job log. Log operational metadata such as external case ID, job ID, submission ID, status, error code and delivery attempts in your system. Avoid logging original files or credentials. The export disables saved successful/error executions and manual-execution history; review your instance-wide overrides, queue persistence, waiting executions, crash dumps and backups. Those controls belong to your n8n operator and are separate from DeepfakePolicy's retention.

## Make: manual HTTP recipe

This is a configuration recipe for Make, not an importable Make blueprint. It uses two scenarios so the incoming webhook does not wait for analysis. No Make scenario or CRM connection is created by downloading this guide.

**Scenario A — accept and submit**

1. Add **Webhooks → Custom webhook**. Enable its built-in API-key authentication; your sending server supplies `X-Make-Apikey`. Define the input structure explicitly using the same `externalCaseId`, `submissionId` and `comparison` fields shown above. Accept one company's trusted server and configure one company's DeepfakePolicy credential per scenario. Enable sequential processing to prevent concurrent metadata updates. Make's custom webhook has a **5 MiB request limit**, including base64 overhead. For larger bundles, pass attachment IDs and fetch them from your fixed, authenticated case system inside Make; the Cross-check API still receives file bytes, never arbitrary URLs. [Make webhook setup](https://apps.make.com/gateway)
2. Use a **Data store** record keyed by a fixed company reference plus `submissionId`. Persist `externalCaseId`, the submission ID, creation time and submission-attempt count before calling the API. Reject reuse of a submission ID with a different external case ID. Always reuse the same ID and bytes after an interrupted submission; do not generate a UUID inside a retrying HTTP module.
3. Add **HTTP v4 → Make a request** with this configuration. [Make HTTP module](https://apps.make.com/http)

   | Setting | Value |
   | --- | --- |
   | Method / URL | `POST https://deepfakepolicy.com/v1/comparisons` |
   | Authentication | API key credential: placement **Header**, parameter name `Authorization`, full key value `Bearer YOUR_LIVE_COMPANY_KEY` in the credential keychain |
   | Header | `Idempotency-Key: make-cross-check:SUBMISSION_ID` mapped from the persisted ID |
   | Body | `application/json`, data structure mapped to the incoming **comparison object only** |
   | Parse response | Yes |
   | Redirects | No |
   | Timeout | 30 seconds |
   | Failed HTTP requests | Return error; attach the error routes described below |

   The key requires `jobs:create` and `jobs:read`. The API validates the complete comparison, its files and any company template. Keep the actual API key in Make's [credential keychain](https://apps.make.com/api-key-authentication-type), not a plain header value or exported scenario variable.
4. On API `200`/`202`, save `jobId`, actual `status`, `retentionExpiresAt`, `pollAttempts: 0`, `nextPollAt` and a polling deadline one hour from submission. Preserve the original case mapping. Finish with **Webhook response**, status `202` for a pending job or `200` for a terminal one, body `{externalCaseId, submissionId, jobId, status}` and content type `application/json`. A missing/lost response is recovered by the same idempotent submission. Do not treat Make's generic “Accepted” response as evidence that a paid API job exists.

**Scenario B — retrieve and deliver**

Run on the shortest [schedule supported by your Make plan](https://help.make.com/schedule-a-scenario). Search due metadata records that have a `jobId` and have not finished delivery. For each, use **HTTP → Make a request** with the same API credential: `GET https://deepfakepolicy.com/v1/jobs/JOB_ID`, no request body, redirects off. Increment the persisted poll counter on every attempted read.

- `queued`, `processing` or `uploading`: set the next due time; stop automated polling after **60 attempts or the one-hour deadline**. Record `polling_timeout`; it does not cancel the job. A manual follow-up resets only the polling window and reads the same job ID.
- `awaiting_upload`: stop polling and record `upload_required`. Have the sending server retry Scenario A with the original submission ID, identical files/parameters and the same API key to finish the existing upload. Scenario B must not create a replacement analysis; GET cannot supply missing bytes.
- `completed`: require `result.comparison.schemaVersion === 1`. Map `{schemaVersion:"deepfakepolicy.integration.v1", externalCaseId, jobId, status, comparison:result.comparison, retentionExpiresAt, deliveryKey:"dfp-cross-check:JOB_ID:completed"}` to your configured CRM update or HTTP destination. Verify its existing case-to-job mapping and upsert by job ID. Retain findings and citations; do not convert a discrepancy into an automatic rejection.
- `failed`: record the API error code, route the case for review and stop. Preserve `credit_refund_pending` as pending reconciliation. `deleted`, `deletion_pending`, HTTP `404`/`410`, an unknown state or a non-comparison result also stop without creating another analysis.
- A failed CRM delivery retries only that delivery, at most three times, with the same delivery key. You may retrieve the saved job again before its seven-day expiry. Scenario B must contain **no POST to `/v1/comparisons`**. Record `delivery_failed` after the retry limit, then require an operator to resume delivery.

**HTTP error routes for both scenarios:** honor `Retry-After` on `429` and persist `nextAttemptAt`. For an existing job, Scenario B reads it on a later scheduled run. If submission has no confirmed job ID, respond with the actual `429`/`503` and retry delay so the sending server resends the identical request to Scenario A after that delay; Scenario B never submits it. Limit network/`408`/`5xx` retries to three attempts per operation with bounded backoff. A submit retry must use the unchanged request, original submission ID and same API key. Return other `4xx` errors for correction, including `409 idempotency_conflict`; never switch to a new ID automatically. Save only operational metadata in the recovery store; retrieve pending submission bytes again from your controlled case system when needed. Apply company spending limits before enabling live submission, and review Make's queue/history retention separately from DeepfakePolicy retention.

## Verification and official references

Repository check: `node --test docs/integrations/n8n-workflow.test.mjs` exercises the exact exported Code/IF/Wait/Respond graph with a local HTTP mock. It checks submit/poll/correlation, a lost accepted response with one idempotent job, status-only reads, resume delivery, conflicts, expiry, processing failure, bounded polling, rate limits, malformed input and secret-free inactive export. It performs no paid calls and does not certify your installed n8n runtime or CRM mapping.

Regenerate the export after changing its source: `node docs/integrations/build-n8n-workflow.mjs`.

Official n8n documentation consulted for the exported node contract:

- [Webhook authentication](https://docs.n8n.io/integrations/builtin/credentials/webhook/)
- [HTTP Request authentication, full responses and timeouts](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/)
- [Respond to Webhook and its input-data output](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.respondtowebhook/)
- [Wait intervals and execution persistence](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/)
- [Official HTTP Request node versions](https://github.com/n8n-io/n8n/blob/master/packages/nodes-base/nodes/HttpRequest/HttpRequest.node.ts)

DeepfakePolicy contract: https://deepfakepolicy.com/v1/openapi.json
