Xbow Resources API
Upload and manage files used in assessments, such as source code archives. All endpoints require an _organization_ API key. ## Upload flow Resources use a multipart S3 upload. The full flow is: 1. **Create** — `POST /api/v1/organizations/:organizationId/resources` — initiates an upload and returns a resource ID. Status is `initiated`. 2. **Get part URLs** — `POST /api/v1/resources/:resourceId/parts` — provide a list of part numbers and receive a corresponding list of presigned S3 `PUT` URLs. 3. **Upload parts** — `PUT` each part directly to its presigned URL. Save the `ETag` header from each part response. 4. **Commit** — `POST /api/v1/resources/:resourceId/commit` — submit the part numbers and ETags. Optionally include a `sha256` checksum of the full file for server-side integrity verification. Status moves to `processing`. 5. **Poll** — `GET /api/v1/resources/:resourceId` — wait until status is `ready` (or `failed`). 6. **Delete** — `DELETE /api/v1/resources/:resourceId` — the resource can be manually deleted when required. Status moves to `deleted`. The maximum part size is 5 GiB. Parts must be at least 5 MiB each, except the final part which may be smaller. Breaking large files into multiple parts allows failed parts to be retried individually rather than restarting the entire upload. ## Resource statuses | Status | Meaning | |---|---| | `initiated` | Upload in progress — parts not yet committed | | `processing` | Commit received — server is validating and storing | | `ready` | Available to use in assessments | | `failed` | Processing failed — see `statusMessage` for details | | `deleted` | Deleted | ## Example This snippet shows how to upload an example file named `source.tar.gz` using the API multipart upload feature. It uses the `fetch` API and assumes a Node.js environment with `fs/promises` available. ```typescript import fs from "node:fs/promises"; import crypto from "node:crypto"; const BASE = "https://console.xbow.com"; const ORG_ID = "your-organization-id"; const API_KEY = "your-api-key"; const PART_SIZE = 5 * 1024 * 1024; // 5 MiB minimum const API_VERSION = "next"; const headers = { "Authorization": `Bearer ${API_KEY}`, "X-XBOW-API-Version": `${API_VERSION}` }; // 1. Create resource const created = await fetch(`${BASE}/api/v1/organizations/${ORG_ID}/resources`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ name: "My source", fileName: "source.tar.gz", type: "source" }), }).then(r => r.json()); const resourceId = created.id; // 2. Split file into parts and request presigned URLs const file = await fs.readFile("source.tar.gz"); const partCount = Math.ceil(file.length / PART_SIZE); const { parts: partUrls } = await fetch(`${BASE}/api/v1/resources/${resourceId}/parts`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ parts: Array.from({ length: partCount }, (_, i) => i + 1) }), }).then(r => r.json()); // 3. Upload each part to S3 directly, collect ETags const uploadedParts = await Promise.all(partUrls.map(async ({ partNumber, url }) => { const chunk = file.slice((partNumber - 1) * PART_SIZE, partNumber * PART_SIZE); const res = await fetch(url, { method: "PUT", body: chunk }); return { partNumber, eTag: res.headers.get("ETag").replaceAll('"', "") }; })); // 4. Commit (sha256 is optional but recommended for integrity verification) const sha256 = crypto.createHash("sha256").update(file).digest("hex"); await fetch(`${BASE}/api/v1/resources/${resourceId}/commit`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ parts: uploadedParts, sha256 }), }).then(r => r.json()); // 5. Poll until ready let resource; do { await new Promise(r => setTimeout(r, 5000)); resource = await fetch(`${BASE}/api/v1/resources/${resourceId}`, { headers }).then(r => r.json()); } while (resource.status === "processing" || resource.status === "initiated"); if (resource.status !== "ready") throw new Error(`Upload failed: ${resource.statusMessage}`); console.log("Resource ready:", resource.id); // 6. Delete when no longer required await fetch(`${BASE}/api/v1/resources/${resourceId}`, { method: "DELETE", headers, }); ```