Recoup

Get started with Recoup.

API REFERENCE

Upload files

On this page
POST/api/sandboxes/files

Upload one or more files to the authenticated account's Git-backed repository. Each file URL is committed separately; paths inside a submodule resolve to that repository. Authenticate with x-api-key or an Authorization Bearer token.

For a guarded update of an existing file: 1. Read it with GET /api/sandboxes/file?include_revision=true and retain the returned sha with that draft. 2. Upload the edited bytes using the staged-file upload, then send the resulting URL and the original SHA as files[].expected_sha. Guarded updates save to main. A conflicting save is not retried automatically. 3. On a conflict, preserve the unsaved draft, reread the latest content and reconcile explicitly. Do not fetch a new SHA merely to force an older draft through.

A batch is not a transaction. HTTP 200 can include successful uploaded receipts alongside errors and conflicts; inspect every file. If every upload fails only because its condition conflicts, the response is 409. If all fail with mixed conflict/other failures, it is 500 and keeps the conflicts list. Conflicts contain submitted file names; successes include resulting blob SHAs.

Temporary upload blobs are deleted after every attempt, including conflicts, so retrying requires a new upload URL. Omitting expected_sha creates or updates the file on the repository default branch without checking the version you opened. The condition only supports updates to existing files. It protects saves that supply it; saving here does not update files in an already running sandbox.

If the target was deleted after the read, do not retry without the condition. This upload has no dedicated missing-target status: non-conflict failures return 500 when no file succeeds, or appear in errors alongside successful receipts in a 200 partial result. A missing target is not guaranteed to return 404 or 409; reread the path before deciding how to recover.

The optional account_id on list/file reads does not delegate uploads or replacements. This upload route remains bound to its existing authenticated-account ownership checks; do not reuse a different account's draft or SHA here. File deletion behavior is also unchanged.

Authentication

See the authentication guide for API key and account access requirements.

Request

cURL
curl --request POST \
  --url 'https://api.recoupable.dev/api/sandboxes/files' \
  --header 'x-api-key: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "path": "drafts",
  "message": "Update release notes",
  "files": [
    {
      "name": "notes.txt",
      "url": "https://example.com/staged/notes.txt",
      "expected_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    }
  ]
}'

Try it

Fill in the fields, send the request from your browser, and read the live response. The curl below updates as you type.

Kept in this browser tab only and cleared when it closes.

cURL for this request
curl --request POST \
  --url 'https://api.recoupable.dev/api/sandboxes/files' \
  --header 'Content-Type: application/json' \
  --data '{
  "path": "drafts",
  "message": "Update release notes",
  "files": [
    {
      "name": "notes.txt",
      "url": "https://example.com/staged/notes.txt",
      "expected_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    }
  ]
}'

Request body required

JSON body containing file URLs and target path

application/json

pathstring

Target directory within the resolved repository; omitted or empty means root. Use a canonical relative path. Dot/traversal segments, backslashes, control characters, malformed Unicode and leading/trailing/doubled slashes return 400. These checks also apply to legacy requests.

Example: "assets/images"

filesarray<object>required

Array of files to upload, each with a URL and target filename

minItems: 1

Item properties for files
urlstringrequired

The URL of the file to upload

Example: "https://example.com/files/album-cover.png"

format: uri

namestringrequired

Nonempty relative file name/path under the target directory. Use a canonical relative path. Dot/traversal segments, backslashes, control characters, malformed Unicode and leading/trailing/doubled slashes return 400. These checks also apply to legacy requests.

Example: "album-cover.png"

minLength: 1

expected_shastring

Optional observed blob SHA from the matching versioned read. When provided, update the existing file on main only if its current contents still match. Omission retains unguarded legacy create/update behavior; no automatic conflict retry occurs. See the upload operation's missing-target recovery guidance before retrying a failed save.

Example: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

pattern: ^[a-f0-9]{40}$

messagestring

Optional commit message. Defaults to 'Upload files via API'.

Example: "Add new album artwork"

Responses

200At least one file was saved. Partial success also returns errors and optional conflicts; keep every successful receipt and inspect every failure.

application/json

statusstring · enumrequired

Status of the operation

Values: "success"

uploadedarray<object>required

Array of uploaded file details

minItems: 1

Item properties for uploaded
pathstringrequired

The full path of the uploaded file in the repository

shastringrequired

Blob SHA of the newly saved file. Retain it as the next observed condition when editing these saved bytes.

Example: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"

pattern: ^[a-f0-9]{40}$

errorsarray<string>

Per-file failures retained alongside successful receipts. HTTP 200 does not mean every file was saved.

Item properties for errors

string

conflictsarray<string>

Submitted names of files rejected because their observed revision no longer matches.

minItems: 1

Item properties for conflicts

string

All requested files saved

success
{
  "status": "success",
  "uploaded": [
    {
      "path": "drafts/notes.txt",
      "sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    }
  ]
}

One file saved and another conflicted

partialConflict
{
  "status": "success",
  "uploaded": [
    {
      "path": "drafts/saved.txt",
      "sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    }
  ],
  "errors": [
    "notes.txt: File changed"
  ],
  "conflicts": [
    "notes.txt"
  ]
}

400Invalid JSON, empty files, noncanonical path/name, or expected_sha not exactly 40 lowercase hexadecimal characters. Empty root directory is allowed; empty file names are not.

application/json

errorstringrequired

Error message describing what went wrong

Example: "Failed to create sandbox"

401Missing or invalid API-key/Bearer authentication.

application/json

errorstringrequired

Error message describing what went wrong

Example: "Failed to create sandbox"

403Forbidden - account does not have access

application/json

errorstringrequired

Error message describing what went wrong

Example: "Failed to create sandbox"

404Not found - no snapshot or no github_repo configured

application/json

errorstringrequired

Error message describing what went wrong

Example: "Failed to create sandbox"

409Every upload failed with a revision conflict. No file was saved. Keep the draft and reconcile with the latest content before uploading again.

application/json

statusstring · enumrequired

Values: "error"

errorstringrequired

Combined upload failure description.

conflictsarray<string>required

Submitted names of files rejected because their observed revision no longer matches.

minItems: 1

Item properties for conflicts

string

409 example
{
  "status": "error",
  "error": "All uploads failed: notes.txt: File changed",
  "conflicts": [
    "notes.txt"
  ]
}

500All uploads failed and at least one failure was not a revision conflict. Mixed failures retain conflicts; other total failures may omit them.

application/json

statusstring · enumrequired

Values: "error"

errorstringrequired

Combined upload failure description.

conflictsarray<string>

Submitted names of files rejected because their observed revision no longer matches.

minItems: 1

Item properties for conflicts

string

500 example
{
  "status": "error",
  "error": "All uploads failed: notes.txt: File changed; cover.png: Download failed",
  "conflicts": [
    "notes.txt"
  ]
}

Full specification

Download the OpenAPI file for complete schemas, constraints, and examples.

Download content.json
View operation source
json
{
  "operationId": "postSandboxesFiles",
  "description": "Upload one or more files to the authenticated account's Git-backed repository. Each file URL is committed separately; paths inside a submodule resolve to that repository. Authenticate with `x-api-key` or an Authorization Bearer token.\n\nFor a guarded update of an existing file:\n1. Read it with `GET /api/sandboxes/file?include_revision=true` and retain the returned `sha` with that draft.\n2. Upload the edited bytes using the [staged-file upload](/api-reference/sandboxes/stage-file), then send the resulting URL and the original SHA as `files[].expected_sha`. Guarded updates save to `main`. A conflicting save is not retried automatically.\n3. On a conflict, preserve the unsaved draft, reread the latest content and reconcile explicitly. Do not fetch a new SHA merely to force an older draft through.\n\nA batch is not a transaction. HTTP 200 can include successful `uploaded` receipts alongside `errors` and `conflicts`; inspect every file. If every upload fails only because its condition conflicts, the response is 409. If all fail with mixed conflict/other failures, it is 500 and keeps the conflicts list. Conflicts contain submitted file names; successes include resulting blob SHAs.\n\nTemporary upload blobs are deleted after every attempt, including conflicts, so retrying requires a new upload URL. Omitting `expected_sha` creates or updates the file on the repository default branch without checking the version you opened. The condition only supports updates to existing files. It protects saves that supply it; saving here does not update files in an already running sandbox.\n\nIf the target was deleted after the read, do not retry without the condition. This upload has no dedicated missing-target status: non-conflict failures return 500 when no file succeeds, or appear in `errors` alongside successful receipts in a 200 partial result. A missing target is not guaranteed to return 404 or 409; reread the path before deciding how to recover.\n\nThe optional `account_id` on list/file reads does not delegate uploads or replacements. This upload route remains bound to its existing authenticated-account ownership checks; do not reuse a different account's draft or SHA here. File deletion behavior is also unchanged.",
  "requestBody": {
    "description": "JSON body containing file URLs and target path",
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/UploadSandboxFilesRequest"
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "At least one file was saved. Partial success also returns errors and optional conflicts; keep every successful receipt and inspect every failure.",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/UploadSandboxFilesResponse"
          },
          "examples": {
            "success": {
              "summary": "All requested files saved",
              "value": {
                "status": "success",
                "uploaded": [
                  {
                    "path": "drafts/notes.txt",
                    "sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
                  }
                ]
              }
            },
            "partialConflict": {
              "summary": "One file saved and another conflicted",
              "value": {
                "status": "success",
                "uploaded": [
                  {
                    "path": "drafts/saved.txt",
                    "sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
                  }
                ],
                "errors": [
                  "notes.txt: File changed"
                ],
                "conflicts": [
                  "notes.txt"
                ]
              }
            }
          }
        }
      }
    },
    "400": {
      "description": "Invalid JSON, empty files, noncanonical path/name, or expected_sha not exactly 40 lowercase hexadecimal characters. Empty root directory is allowed; empty file names are not.",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/SandboxErrorResponse"
          }
        }
      }
    },
    "401": {
      "description": "Missing or invalid API-key/Bearer authentication.",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/SandboxErrorResponse"
          }
        }
      }
    },
    "403": {
      "description": "Forbidden - account does not have access",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/SandboxErrorResponse"
          }
        }
      }
    },
    "404": {
      "description": "Not found - no snapshot or no github_repo configured",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/SandboxErrorResponse"
          }
        }
      }
    },
    "409": {
      "description": "Every upload failed with a revision conflict. No file was saved. Keep the draft and reconcile with the latest content before uploading again.",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/SandboxFileConflictResponse"
          },
          "example": {
            "status": "error",
            "error": "All uploads failed: notes.txt: File changed",
            "conflicts": [
              "notes.txt"
            ]
          }
        }
      }
    },
    "500": {
      "description": "All uploads failed and at least one failure was not a revision conflict. Mixed failures retain conflicts; other total failures may omit them.",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/SandboxUploadErrorResponse"
          },
          "example": {
            "status": "error",
            "error": "All uploads failed: notes.txt: File changed; cover.png: Download failed",
            "conflicts": [
              "notes.txt"
            ]
          }
        }
      }
    }
  }
}