Skip to main content

Initialize a File Upload and Attach It to an Intake

Goal

Upload a file into your SpotDraft workspace through the public API, then use the returned file_id while creating a legal intake.

Endpoints used

  • POST /api/v2.1/public/workspace/files/uploads/
  • POST /api/v1/public/legal_intake/
  • GET /api/v2.1/public/workspace/files/{file_id}/

Prerequisites

  • a valid public API key for the target workspace
  • a workflow id for an INTAKE_WORKFLOW
  • the local file you want to upload
  • a content type that matches the file, such as application/pdf

Overview

This flow has three steps:

  1. initialize the upload with SpotDraft
  2. upload the raw bytes to the returned storage URL
  3. create the intake using the returned file_id

Request sequence

1. Initialize the upload

Call the workspace file upload API with the file metadata. SpotDraft returns a file_id, a provider-signed upload_url, and an expires_at timestamp.

import os
import requests

base_url = "https://api.in.spotdraft.com" # Replace with your workspace region.
headers = {
"client-id": SPOTDRAFT_CLIENT_ID,
"client-secret": SPOTDRAFT_CLIENT_SECRET,
"Content-Type": "application/json",
}

upload_init = requests.post(
f"{base_url}/api/v2.1/public/workspace/files/uploads/",
headers=headers,
json={
"files": [
{
"filename": "vendor-nda.pdf",
"content_type": "application/pdf",
}
]
},
timeout=30,
).json()

Example response:

{
"files": [
{
"file_id": "10f9a299-9e9a-4bc9-9299-5b89bc06d370",
"filename": "vendor-nda.pdf",
"upload_url": "https://storage.example.com/...",
"expires_at": "2026-04-17T14:30:00Z"
}
]
}

Extract:

  • file_id: the SpotDraft workspace file id you will later attach to the intake
  • upload_url: the signed storage URL that receives the raw bytes

2. Upload the file bytes to the returned URL

The upload_url is not another SpotDraft API endpoint. It is a time-limited signed storage URL. Send the file directly to that URL and do not attach your SpotDraft API headers.

curl --request PUT \
--url "$UPLOAD_URL" \
--header "Content-Type: application/pdf" \
--upload-file ./vendor-nda.pdf

Practical rules:

  • upload before expires_at
  • send the same content type you used while initializing the upload
  • do not add client-id, client-secret, or other SpotDraft API headers to the signed URL request

Production notes

  • The signed upload_url is storage-provider specific and expires quickly.
  • A successful intake response includes the uploaded file under attachments.

Pass the returned file_id in file_ids when creating the intake.

workflow_id = int(os.environ["SPOTDRAFT_WORKFLOW_ID"])
primary_assignee_id = os.environ["SPOTDRAFT_PRIMARY_ASSIGNEE_ID"]
collaborator_id = os.environ["SPOTDRAFT_COLLABORATOR_ID"]

intake = requests.post(
f"{base_url}/api/v1/public/legal_intake/",
headers=headers,
json={
"title": "Review vendor NDA",
"description": "Upload and route a third-party NDA for legal review.",
"priority": "HIGH",
"workflow_id": workflow_id,
"due_date": "2026-04-30T17:00:00Z",
"file_ids": ["10f9a299-9e9a-4bc9-9299-5b89bc06d370"],
"assignees": [
{
"entity_id": primary_assignee_id,
"entity_type": "ORG_USER",
"assignee_type": "ASSIGNED_TO",
},
{
"entity_id": collaborator_id,
"entity_type": "ORG_USER",
"assignee_type": "COLLABORATOR",
},
],
"metadata": {
"source": "developer-portal-recipe",
"request_type": "nda-review",
},
},
timeout=30,
).json()

Example response excerpt:

{
"id": 1,
"public_id": "dc676f34-0c1a-4f89-8d24-5264d4f8b735",
"reference_id": "I-001",
"title": "Review vendor NDA",
"attachments": [
{
"name": "vendor-nda.pdf",
"uuid": "10f9a299-9e9a-4bc9-9299-5b89bc06d370"
}
]
}

4. Optionally fetch a signed download URL for the uploaded file

After the file is attached, you can request a signed download URL using the same file_id.

signed_url = requests.get(
f"{base_url}/api/v2.1/public/workspace/files/10f9a299-9e9a-4bc9-9299-5b89bc06d370/",
headers=headers,
timeout=30,
).json()

Example response:

{
"signed_url": "https://storage.example.com/download/..."
}

Common failure points

  • 400 One or more file_ids not found in workspace This usually means the upload was initialized in a different workspace, or you used the wrong file_id.

  • 400 This workflow has no published version for the workspace The workflow_id must resolve to a published frozen version in the current workspace.

  • 400 This workflow is not an INTAKE_WORKFLOW File attachment during intake creation only works with intake workflows.

  • upload URL expired Re-run the upload initialization step to get a fresh upload_url.

  • wrong content type Use the same MIME type in the upload request that you used during initialization.