> ## Documentation Index
> Fetch the complete documentation index at: https://chainpatrol.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload Attachment

> Upload an evidence file and receive a URL for it. Pass that URL to `POST /report/create` in `attachmentUrls` to attach the file to a report — uploading alone does not create or modify a report.

Send either the raw file bytes with a `fileName` query parameter, or a `multipart/form-data` body with up to 10 file parts. The whole request body must be 4194304 bytes or smaller.

The file extension decides how the file is stored and served, so it must be one of: .avif, .bmp, .csv, .doc, .docx, .eml, .gif, .jpeg, .jpg, .msg, .pdf, .png, .txt, .webp. Any `Content-Type` header you send is ignored in favour of the extension.

Not images only: `.eml` and `.msg` messages, PDFs and documents are all accepted. Images are displayed inline on the report; other files are listed as downloadable evidence.

## Overview

Upload a file to ChainPatrol and get back a URL. Pass that URL to
[Create Report](/docs/external-api/report-create) in `attachmentUrls` to attach the file as
evidence.

This is a two-step flow, and the two steps are separate requests:

1. `POST /attachment/upload` — send the bytes, receive a URL.
2. `POST /report/create` — include that URL in `attachmentUrls`.

See [Creating Reports](/docs/external-api/creating-reports) for the complete flow with worked
examples.

<Info>
  **You do not have to use this endpoint.** `attachmentUrls` accepts any publicly
  reachable URL, so if you already host your evidence somewhere we can fetch, keep passing
  those links. This endpoint exists so you do not *have* to host them.
</Info>

<Note>
  **It is not images only.** Screenshots are the most common attachment, but `.eml` and
  `.msg` message files, PDFs, Word documents, CSVs, and plain text are all supported — see
  [Supported file types](#supported-file-types). Images are displayed inline on the report
  in the dashboard; everything else is listed as a downloadable file that our takedown team
  can forward to a registrar or host.
</Note>

Read-only API keys cannot upload — that role exists for cross-organization reads. Uploads
are recorded against the organization behind the key. See
[Authentication](/docs/external-api/authentication) for how to send your key.

## Two ways to send a file

The endpoint accepts either shape. Pick whichever your HTTP client makes easier.

### Raw body

Send the file bytes as the request body and the file name as the `fileName` query
parameter. One file per request.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://app.chainpatrol.io/api/v2/attachment/upload?fileName=evidence.eml' \
    -H 'X-API-KEY: YOUR_API_KEY_HERE' \
    --data-binary '@evidence.eml'
  ```

  ```typescript TypeScript theme={null}
  import { readFile } from "node:fs/promises";

  const bytes = await readFile("evidence.eml");

  const response = await fetch(
    "https://app.chainpatrol.io/api/v2/attachment/upload?fileName=evidence.eml",
    {
      method: "POST",
      headers: { "X-API-KEY": "YOUR_API_KEY_HERE" },
      body: bytes,
    }
  );

  const { attachments } = await response.json();
  console.log(attachments[0].url);
  ```

  ```python Python theme={null}
  import requests

  with open("evidence.eml", "rb") as file:
      response = requests.post(
          "https://app.chainpatrol.io/api/v2/attachment/upload",
          params={"fileName": "evidence.eml"},
          headers={"X-API-KEY": "YOUR_API_KEY_HERE"},
          data=file,
      )

  attachments = response.json()["attachments"]
  print(attachments[0]["url"])
  ```
</CodeGroup>

<Warning>
  `fileName` is required for raw uploads, and must appear exactly once. Repeating it
  (`?fileName=a.eml&fileName=b.eml`) is rejected with a `400`.
</Warning>

### Multipart form

Send `multipart/form-data` with one or more file parts. This is what a browser form,
Postman's file picker, or `curl -F` produces.

The file name comes from each part's own filename, so the `fileName` query parameter is
ignored here. Non-file form fields are ignored, and the part names themselves do not
matter — send them all as `file` if you like.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://app.chainpatrol.io/api/v2/attachment/upload' \
    -H 'X-API-KEY: YOUR_API_KEY_HERE' \
    -F 'file=@evidence.eml' \
    -F 'file=@screenshot.png'
  ```

  ```typescript TypeScript theme={null}
  const form = new FormData();
  form.append("file", new File([emlBytes], "evidence.eml"));
  form.append("file", new File([pngBytes], "screenshot.png"));

  const response = await fetch(
    "https://app.chainpatrol.io/api/v2/attachment/upload",
    {
      method: "POST",
      headers: { "X-API-KEY": "YOUR_API_KEY_HERE" },
      body: form,
    }
  );

  const { attachments } = await response.json();
  console.log(attachments.map((attachment) => attachment.url));
  ```

  ```python Python theme={null}
  import requests

  with open("evidence.eml", "rb") as eml, open("screenshot.png", "rb") as png:
      response = requests.post(
          "https://app.chainpatrol.io/api/v2/attachment/upload",
          headers={"X-API-KEY": "YOUR_API_KEY_HERE"},
          files=[
              ("file", ("evidence.eml", eml)),
              ("file", ("screenshot.png", png)),
          ],
      )

  attachments = response.json()["attachments"]
  print([attachment["url"] for attachment in attachments])
  ```
</CodeGroup>

## File name handling

* The extension decides how we store and serve the file, so it has to be present and
  accepted. Matching is case-insensitive.
* Directory components are stripped: `../../secrets/evidence.eml` is stored as
  `evidence.eml`.
* Names must contain no control characters, and are length-capped — the schema above gives
  the limit.
* Your name is otherwise preserved as-is. That matters because it is the name shown on the
  report, and the name a registrar or hosting provider sees when our takedown team forwards
  the file — so `phishing-email.eml` is worth more than `attachment1.eml`.
* Uploading two files with the same name is fine. Each upload is stored on its own path, so
  nothing is ever overwritten and the name you sent is what comes back.

## Supported file types

The accepted extensions are enumerated in the request description above, and that list is
generated from the same constants the endpoint enforces — so it is always current.

The content type we serve the file with is derived from the extension, **not** from the
`Content-Type` header you send, so any header your client sets is ignored. `.svg` and
`.html` are deliberately excluded: they render as active content, and files we serve should
not.

If you need a format that is not accepted, email
[support@chainpatrol.io](mailto:support@chainpatrol.io) — a common workaround is to send
the file inside a supported container (a PDF export, or the raw `.eml`).

## Limits

The request body size cap and the per-request file count are stated in the schema above.
Two things it does not tell you:

* **The size cap applies to the whole request body**, so in a multipart request all parts
  share one budget. For anything larger, split it across requests and attach the resulting
  URLs to the same report.
* **Uploads are rate limited to 60 requests per minute** per organization, or per API key
  for keys that are not tied to one. Exceeding it returns `429`.

## The returned URL

<Warning>
  Treat the `url` as opaque. Store it and pass it through — do not parse it, and do not try
  to construct one yourself. The path layout is an implementation detail and can change.

  The URL is served from our CDN and is reachable by anyone who has it, which is what lets
  us forward evidence to registrars and hosting providers. The path is not guessable, but
  do not upload anything you would not want a third party to see if the link were shared.
</Warning>

## Error responses

Every error returns a JSON body with a single `error` string describing what was wrong.
The statuses are listed with the responses above; two things worth calling out:

* **`405` is not in the list**, because it is not a response of `POST`. The endpoint is
  `POST` only, and any other method returns `405 Method Not Allowed`.
* **Retry `429` and `502`, not `400`.** A `4xx` other than `429` means the request itself
  needs fixing, and retrying it unchanged will fail the same way.

## Best practices

* **Upload first, report second.** Nothing links the file to a report until you pass the
  URL to `report.create`. An uploaded file that is never referenced simply sits unused.
* **Attach every file to a report.** Uploads are not visible in the dashboard on their
  own; the report is what makes them reviewable.
* **Use meaningful file names.** The name follows the file into the dashboard and into
  outbound takedown emails.
* **Send `.eml` rather than a screenshot of an email** when you have it. The raw message
  carries headers, which is what registrars and email providers act on.

## Notes

* There is no endpoint for listing, replacing, or deleting an upload. To correct a
  mistake, upload the right file and reference that URL instead; to remove evidence from a
  report, contact [support@chainpatrol.io](mailto:support@chainpatrol.io).
* Files are not scanned for malware. Do not treat an uploaded file as safe to open just
  because it came through the API.


## OpenAPI

````yaml POST /attachment/upload
openapi: 3.0.3
info:
  title: ChainPatrol External API - OpenAPI 3.0
  description: ChainPatrol External API documentation
  version: 2.0.0
servers:
  - url: https://app.chainpatrol.io/api/v2
security: []
tags:
  - name: asset
  - name: report
externalDocs:
  url: https://chainpatrol.com/docs
paths:
  /attachment/upload:
    post:
      tags:
        - report
      summary: Upload an attachment
      description: >-
        Upload an evidence file and receive a URL for it. Pass that URL to `POST
        /report/create` in `attachmentUrls` to attach the file to a report —
        uploading alone does not create or modify a report.


        Send either the raw file bytes with a `fileName` query parameter, or a
        `multipart/form-data` body with up to 10 file parts. The whole request
        body must be 4194304 bytes or smaller.


        The file extension decides how the file is stored and served, so it must
        be one of: .avif, .bmp, .csv, .doc, .docx, .eml, .gif, .jpeg, .jpg,
        .msg, .pdf, .png, .txt, .webp. Any `Content-Type` header you send is
        ignored in favour of the extension.


        Not images only: `.eml` and `.msg` messages, PDFs and documents are all
        accepted. Images are displayed inline on the report; other files are
        listed as downloadable evidence.
      operationId: attachmentUpload
      parameters:
        - name: fileName
          in: query
          required: false
          description: >-
            File name for the uploaded bytes, including the extension (for
            example `evidence.eml`). Required for raw-body uploads, and must be
            supplied exactly once. Ignored for multipart requests, where each
            part carries its own file name.
          schema:
            type: string
            maxLength: 200
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
              description: The raw file bytes. Name the file with `?fileName=`.
          multipart/form-data:
            schema:
              type: object
              description: >-
                One or more file parts, at most 10. Part names are not
                significant and non-file fields are ignored, so any field name
                works.
              properties:
                file:
                  type: array
                  items:
                    type: string
                    format: binary
                  description: The files to upload.
      responses:
        '200':
          description: The files were stored.
          content:
            application/json:
              schema:
                type: object
                properties:
                  attachments:
                    type: array
                    description: One entry per uploaded file, in request order.
                    items:
                      type: object
                      properties:
                        url:
                          type: string
                          format: uri
                          description: >-
                            URL to pass to `report.create` in `attachmentUrls`.
                            Treat it as opaque — the path layout is an
                            implementation detail.
                        fileName:
                          type: string
                          description: The stored file name, after sanitising.
                        sizeBytes:
                          type: integer
                          description: Size of the stored file in bytes.
                      required:
                        - url
                        - fileName
                        - sizeBytes
                required:
                  - attachments
        '400':
          description: >-
            Unsupported extension, missing or repeated `fileName`, empty body,
            an empty file part, malformed multipart, no file parts, or too many
            files.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    description: >-
                      Human-readable description of what was wrong with the
                      request
                required:
                  - error
        '401':
          description: Missing, invalid, or expired API key.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    description: >-
                      Human-readable description of what was wrong with the
                      request
                required:
                  - error
        '403':
          description: >-
            Read-only API key. Read-only keys exist for cross-organization reads
            and cannot upload.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    description: >-
                      Human-readable description of what was wrong with the
                      request
                required:
                  - error
        '413':
          description: Request body larger than 4194304 bytes.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    description: >-
                      Human-readable description of what was wrong with the
                      request
                required:
                  - error
        '429':
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    description: >-
                      Human-readable description of what was wrong with the
                      request
                required:
                  - error
        '502':
          description: The file could not be stored. Safe to retry.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    description: >-
                      Human-readable description of what was wrong with the
                      request
                required:
                  - error
      security:
        - ApiKey: []
components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-API-KEY
      description: >-
        Your API key. This is required by most endpoints to access our API
        programatically. Reach out to us at
        [support@chainpatrol.io](mailto:support@chainpatrol.io?subject=Re:%20API%20Key%20for%20SDK&body=Company:%20%0AName:%20%0APurpose:%20)
        to get an API key for your use.

````