Title: Pull HTTP protocol
Published: November 3, 2026

---

# Pull HTTP protocol

## In this article

 * [Sending file paths through source firewalls](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#sending-file-paths-through-source-firewalls)
    - [Encoding and decoding a path](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#encoding-and-decoding-a-path)
    - [File-list uploads](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#file-list-uploads)
    - [Cursors sent back to the source](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#cursors-sent-back-to-the-source)
    - [Choosing a format for older servers](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#choosing-a-format-for-older-servers)
 * [Output before the response](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#output-before-the-response)
 * [Test a request-format change](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#test-a-request-format-change)

Use this reference when changing a request or response in the client and server 
together. If you are adding Reprint to a product, use the [client integration guide](https://developer.wordpress.org/reprint/build-with-reprint/)
first; it handles these request details for you. Private cursor and state formats
can change between client versions.

Keep the API routing marker (`?reprint-api` or the legacy `?site-export-api`) in
the URL. The client keeps every parameter of the supplied API URL, including any
custom routing query parameters, and appends `endpoint` to its query. Those query
parameters stay in the URL. Additional export options go in the POST body. For example,
POST to `/?reprint-api&endpoint=db_index` with:

    ```language-json
    {
      "skip_rows": [
        {
          "table_name_without_prefix": "postmeta",
          "column": "meta_key",
          "value_base64": "X2VkaXRfbG9jaw=="
        }
      ]
    }
    ```

The exporter still reads export parameters from the query string, where clients 
sent them before the POST transport. A released token client, recognised by its `
X-Auth-Content-Hash` header, is refused with `client_update_required` before its
request is dispatched. When a parameter appears in both places, the POST value wins
for JSON and form bodies. The endpoint is the exception. The server dispatches the
query’s value. When the query names none, it takes the body’s value unless that 
value is a push endpoint. Released key clients send the endpoint in the body, so
this fallback keeps them working. Existing multipart file-list uploads keep their
file part, and their options may stay in the query string.

For `file_fetch`, send the requested paths in the multipart `file_list` upload. 
Each record’s `path` uses ordinary base64 or [`rp.`-prefixed ROT13-base64](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#sending-file-paths-through-source-firewalls),
depending on the source’s preflight capabilities. The server ignores any `file_list_path`
request parameter. That option remains available to direct PHP callers of `endpoint_file_fetch()`.

Requests are signed over newline-joined fields behind a version label. The connection
token uses `reprint-hmac-sha256-v2`, and a key uses `reprint-rsa-sha256-v1` plus
its key id. The remaining fields are the nonce, the timestamp, the uppercase HTTP
method, and the request’s path and query, which name the endpoint. The signature
never covers or hashes the body, so a multipart upload streams without buffering.

The exporter accepts `application/json`, `application/x-www-form-urlencoded`, and`
multipart/form-data`. The signature covers the URL, endpoint included, and never
the body. Use the [file push reference](https://developer.wordpress.org/reprint/contributing/push/)
for push request fields.

The client sends URL-encoded forms for preflight and streaming pull commands. File-
list downloads retain multipart uploads, with options before the file part. Path
parameters use the [format selected by preflight](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#choosing-a-format-for-older-servers).
Both base64 formats preserve non-UTF-8 filename bytes. Cursors travel only in the
body, which also keeps a large cursor clear of host header size limits. The exporter
still reads an `X-Export-Cursor` header when the body has no cursor, for older clients.
When changing cursor transport, test continuation with that header removed; a source
firewall may discard it.

Multipart array entries use field names such as `directory[0]` and `skip_rows[0][
column]`.

## 󠀁[Sending file paths through source firewalls](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#sending-file-paths-through-source-firewalls)󠁿

For an index request selecting `/tmp`, the client sends `rp.Y3EgpN==` instead of
ordinary base64 (`L3RtcA==`) when the source reports support. Here are the POST 
fields after form decoding:

  |  Field |  Ordinary base64 |  ROT13-base64 with the `rp.` prefix |  
   |  `list_dir` |  `L3RtcA==` |  `rp.Y3EgpN==` |  
 |  `directory[0]` |  `L3RtcA==` |  `rp.Y3EgpN==` |

The same format applies to `pulled_before` and other array entries in these directory
parameters. The endpoint stays in the URL.

### 󠀁[Encoding and decoding a path](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#encoding-and-decoding-a-path)󠁿

The client base64-encodes the path bytes, applies ROT13 to that text, and adds `
rp.`:

    ```php
    $encoded_path = 'rp.' . str_rot13(base64_encode('/tmp'));
    // rp.Y3EgpN==
    ```

The server recognizes the exact `rp.` prefix and reverses those steps:

    ```language-text
    rp.Y3EgpN==  ->  Y3EgpN==  ->  L3RtcA==  ->  /tmp
                 strip rp.      ROT13        base64 decode
    ```

Decoded directory parameters must be absolute paths in the source host’s path format.
Empty values, invalid base64, and relative paths are rejected. A failed `rp.` value
is not tried again as another format. The transform preserves the original bytes,
including Windows paths and non-UTF-8 filename bytes.

The period is outside the standard base64 alphabet. For example, `base64_decode('
rp.Y3EgpN==', true)` returns `false`. The prefix is reserved for this exact format;
it selects the Reprint decoder rather than an ordinary base64 decoder.

The prefix adds no percent escapes to a URL-encoded form. For `/tmp`, the actual
form field is:

    ```language-text
    list_dir=rp.Y3EgpN%3D%3D
    ```

Only the payload’s `+`, `/`, and `=` need the same escaping as ordinary base64. 
Multipart forms carry `rp.Y3EgpN==` without URL encoding.

### 󠀁[File-list uploads](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#file-list-uploads)󠁿

For a download of `/tmp/a.txt`, the `path` in the uploaded `file_list` JSON changes
as follows. Other record fields are omitted:

    ```diff
    -[{"path":"L3RtcC9hLnR4dA=="}]
    +[{"path":"rp.Y3EgpP9uYaE4qN=="}]
    ```

This applies when the source reports support, including runtime-file downloads and
pending batches prepared by an older client. Immediately before upload, the client
writes an encoded temporary copy one entry at a time. Paths already starting with`
rp.` are copied without encoding them again. The input list stays unchanged, and
the temporary copy is removed when the request returns or throws. Newly prepared
batches count the prefix bytes against the request-body budget. The server accepts
both formats, even within one uploaded list.

### 󠀁[Cursors sent back to the source](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#cursors-sent-back-to-the-source)󠁿

A cursor tells the source where to continue. A file response can return the base64
encoding of this JSON:

    ```language-json
    {"path":"L3RtcC9hLnR4dA==","bytes":16384}
    ```

The client keeps the response cursor unchanged in saved state. When the source reports
support, only the copy sent in the next POST changes:

    ```php
    $post_cursor = 'rp.' . str_rot13($response_cursor);
    ```

The cursor is already base64, so the client does not base64-encode it again or change
its nested paths. The server removes `rp.`, undoes ROT13, then runs the existing
base64 and cursor JSON checks. Resuming needs no saved-state conversion.

### 󠀁[Choosing a format for older servers](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#choosing-a-format-for-older-servers)󠁿

Preflight reports `rot13_base64_request_values: true` when the source accepts the
new format. The client selects the following POST formats:

  |  Source reports |  `directory`, `list_dir`, `pulled_before` |  Uploaded file-list paths and POST cursors |  
   |  `rot13_base64_request_values: true` |  `rp.` + ROT13-base64 |  `rp.` + ROT13-base64 |  
 |  Only `base64_path_parameters: true` |  Ordinary base64 |  Ordinary base64 |  
 |  Neither |  Raw paths |  Ordinary base64 |

Preflight itself sends raw paths so it can discover support. New servers still report`
base64_path_parameters: true` and accept ordinary base64 and raw directory paths
for older clients. Protocol version 3 remains unchanged.

Response headers, response cursors, saved indexes, and SQL row-filter values keep
their existing encodings. Push requests and their multipart part headers are unchanged.

This encoding adds no secrecy or authentication. A permissive base64 decoder can
ignore punctuation, and a firewall can learn the transform too. It does not guarantee
acceptance by every firewall. HTTPS and signed requests are unchanged.

## 󠀁[Output before the response](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#output-before-the-response)󠁿

A blank line before `<?php` in the site’s `index.php` can send the HTTP headers 
before Reprint loads. Preflight still returns its JSON object. Streaming pull endpoints
send uncompressed multipart data when they can no longer set headers. The client
skips leading spaces, tabs, and line breaks before detecting the `--boundary-` line
in the body. Part headers carry the cursors and other stream metadata as before.
The client still reads each part as it arrives.

This fallback does not accept HTML or PHP warning text before the boundary. It also
cannot restore an HTTP status or CORS header after headers were sent; a browser 
may still block a cross-origin request. Remove the early output on the source site
to restore those headers and gzip compression.

## 󠀁[Test a request-format change](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#test-a-request-format-change)󠁿

Run the request-body and path-encoding tests from `tests/`:

    ```language-sh
    ../vendor/bin/phpunit Import/MultipartRequestParametersTest.php
    ../vendor/bin/phpunit Import/RequestUrlPathEncodingTest.php
    ../vendor/bin/phpunit Import/KeySignedRequestTest.php
    ```

When changing the negotiated path format, check both a source that advertises support
and one that does not. Include a non-UTF-8 filename and a cursor sent back after
interruption; both must arrive at the source with their original bytes.

First published

November 3, 2026

Last updated

November 3, 2026

Edit article

[ Improve it on GitHub: Pull HTTP protocol ](https://raw.githubusercontent.com/WordPress/reprint/trunk/docs/contributing/pull-http.md)

Changelog

[ See list of changes: Pull HTTP protocol ](https://developer.wordpress.org/reprint/contributing/pull-http/?output_format=md#)

[  Previous: Architecture and durable state](https://developer.wordpress.org/reprint/contributing/architecture/)

[  Next: File push implementation](https://developer.wordpress.org/reprint/contributing/push/)