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

# PIREP File Attachments

> What changes on the wire when a plugin attaches a file to its PIREP submission with attachFile(), and what an airline's backend needs to do about it.

A plugin can send files (a fuel or handling receipt, a loadsheet) inside the PIREP submission itself, by returning them with `attachFile()` from its [`ctx.flight.attachToPirepSubmission`](/sdk/background/context/flight) handler. This page is for whoever maintains the airline's PIREP-receiving backend, not the plugin author.

## What changes

Only one thing changes the PIREP submission: a plugin returning `attachFile()` from its `attachToPirepSubmission` handler. When that happens for a flight, that flight's submission is sent as `multipart/form-data` instead of `application/json`.

Everything else leaves the submission exactly as it has always been, byte-identical JSON. That includes plugins that handle files some other way. A screenshot plugin that uploads each screenshot to its own endpoint as it is taken, for example, never touches the PIREP submission, so your completion endpoint receives the same JSON it always has.

<Note>
  If none of your installed plugins return `attachFile()` from a submission
  handler, nothing on this page applies to you.
</Note>

The switch is decided per submission, not per airline or per plugin. A plugin that only sometimes has a file to attach (no receipts on this leg, say) produces a plain JSON submission on the legs where it returns none.

## Wire format

The PIREP travels as a single form field named `payload`, holding exactly the JSON body your backend already receives when no file is attached. Each attached file travels as its own part, named `<pluginId>_<n>`, numbered from `0` per plugin. Inside the JSON, every attachment is replaced with `{ "$file": "<part name>" }` pointing at its part.

So a submission that would otherwise be:

```json theme={null}
{
  "tracking_id": "ABC123",
  "landing_rate": -142,
  "plugin_data": {
    "ground-ops": {
      "provider_version": "4.1",
      "receipts": [{ "$file": "ground-ops_0" }, { "$file": "ground-ops_1" }]
    }
  }
}
```

arrives as a multipart request with three parts: `payload` carrying that JSON verbatim as a string, plus `ground-ops_0` and `ground-ops_1` carrying the files, each with its own filename and content type.

Recovering it is one step:

```php theme={null}
$pirep = json_decode($request->input('payload'), true);
```

<Note>
  Because the PIREP is one field rather than one field per value, types survive
  exactly as they do on the JSON path: booleans stay booleans, `null` stays
  `null`, and an empty array stays an empty array. Nothing about the body
  changes when a file is attached except that it is wrapped in a `payload`
  field.
</Note>

The alternative, flattening the JSON into bracketed fields like `history[0][lat]`, was deliberately avoided. It emits one field per leaf value, and a two-hour flight produces over 1300 of them. PHP parses a multipart body itself, so [`max_input_vars`](https://www.php.net/manual/en/info.configuration.php#ini.max-input-vars) (default `1000`) applies and silently discards everything past the limit. A JSON body is read in userland and is exempt. Under that encoding, attaching a file to a long flight would have quietly truncated the route, the flight log and the attachment references themselves, leaving the uploaded files with nothing pointing at them.

## What your backend needs to do

1. **Accept `multipart/form-data` on the PIREP completion endpoint**, alongside the `application/json` it already accepts, and read the PIREP from the `payload` field with a JSON decode.
2. **Walk the decoded body for `$file` references** and match each `{ "$file": "<part name>" }` object to the uploaded file part of that name, to reassemble which attachment belongs where.
3. **Check `max_file_uploads`** (PHP default `20`) if you expect plugins to attach many files at once. The PIREP itself is one field regardless of flight length, so `max_input_vars` is not a concern, but each file is still a separate upload.
4. **Return a status you can stand behind** if you reject the submission. See the fallback below for which statuses Stratos treats as "reject the attachments" versus "something went wrong."
5. **Accept a chunked request body.** Because the submission is streamed, Stratos sends no `Content-Length` header, so the request arrives chunked. A proxy or web server in front of your backend that refuses chunked uploads will reject it with 411 or 400 before your application code ever sees it.

## Requirements

File attachments require the flight-tracking plugin at version 1.8.0 or newer, which is the release that forwards a multipart submission to your backend.

Stratos checks this itself: a pilot whose flight-tracking plugin is older simply submits on the JSON path with the attachments omitted, and the client logs why. Your backend never receives a multipart submission from a client that cannot complete one.

## The fallback

If your backend rejects a multipart submission with **400, 413, 415, or 422**, Stratos retries once, automatically, as a plain JSON submission with every attachment stripped out. The flight still files, and the pilot sees a warning that the attachments were rejected.

Any other response, a 5xx, a timeout, or a transport failure, does **not** trigger this retry. Those are ambiguous about whether your backend already committed the PIREP, so retrying could re-file a flight you already accepted. They fall through to Stratos's existing submission rollback instead, and the pilot retries manually.

In short: if you're rejecting a submission specifically because of its attachments (too large, wrong type, a validation rule on the attached fields), use 400/413/415/422 so the pilot's flight still files. Reserve 5xx for genuine backend failures.
