API REFERENCE
Upload files
On this page
/api/sandboxes/filesUpload 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 --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 --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
pathstringTarget 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>requiredArray of files to upload, each with a URL and target filename
minItems: 1
Item properties for files
urlstringrequiredThe URL of the file to upload
Example: "https://example.com/files/album-cover.png"
format: uri
namestringrequiredNonempty 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_shastringOptional 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}$
messagestringOptional 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 · enumrequiredStatus of the operation
Values: "success"
uploadedarray<object>requiredArray of uploaded file details
minItems: 1
Item properties for uploaded
pathstringrequiredThe full path of the uploaded file in the repository
shastringrequiredBlob 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
{
"status": "success",
"uploaded": [
{
"path": "drafts/notes.txt",
"sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
]
}One file saved and another conflicted
{
"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
errorstringrequiredError message describing what went wrong
Example: "Failed to create sandbox"
401Missing or invalid API-key/Bearer authentication.+
application/json
errorstringrequiredError message describing what went wrong
Example: "Failed to create sandbox"
403Forbidden - account does not have access+
application/json
errorstringrequiredError message describing what went wrong
Example: "Failed to create sandbox"
404Not found - no snapshot or no github_repo configured+
application/json
errorstringrequiredError 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 · enumrequiredValues: "error"
errorstringrequiredCombined upload failure description.
conflictsarray<string>requiredSubmitted names of files rejected because their observed revision no longer matches.
minItems: 1
Item properties for conflicts
string
{
"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 · enumrequiredValues: "error"
errorstringrequiredCombined upload failure description.
conflictsarray<string>Submitted names of files rejected because their observed revision no longer matches.
minItems: 1
Item properties for conflicts
string
{
"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.jsonView operation source
{
"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"
]
}
}
}
}
}
}