GraphQL
File uploads
Create a direct-upload target with createFileUpload, PUT the bytes to S3, then pass signedId into a later mutation.
Files are not posted to /graphql as multipart. You create an Active Storage blob, upload bytes to the returned URL, then attach the blob's signedId on a subsequent mutation.
1. Create the upload
createFileUpload is an organization mutation.
mutation CreateFileUpload(
$organizationId: ID!
$input: CreateFileUploadInput!
) {
organization(id: $organizationId) {
createFileUpload(input: $input) {
uploadUrl
uploadHeaders
signedId
errors
}
}
}
Input:
| Field | Type | Description |
|---|---|---|
fileName | String! | Original file name, including extension |
byteSize | Int! | Size in bytes |
checksum | String! | Base64-encoded MD5 of the file body (Active Storage checksum) |
contentType | String | MIME type, for example application/pdf |
Checksum in Ruby is Digest::MD5.base64digest(file_body). In Node:
import { createHash } from 'node:crypto';
const checksum = createHash('md5').update(bytes).digest('base64');
2. PUT the file
uploadUrl is a pre-signed object-storage URL. uploadHeaders is a JSON object of headers you must send with the PUT. Include every header the payload returns; the signature depends on them.
PUT <uploadUrl>
<each key/value from uploadHeaders>
Content-Length: <byteSize>
<raw file bytes>
A successful PUT is 2xx. Do not send a GraphQL token to the storage URL.
3. Attach signedId
Later mutations accept the blob through signed-id fields, for example signedMediaAttachmentFileIds on comments or scoped attachment mutations such as createProjectScopedAttachment / createOrganizationScopedAttachment.
Keep the signedId secret. It is a capability token for that blob.
Scoped attachments
The public schema exposes scoped attachment mutations, not legacy per-record attachment mutations. Use the scoped create/delete fields on OrganizationMutations and ProjectMutations when attaching files to records that support them. Internal names such as createBidAttachment are not part of the public API and return a GraphQL error if you call them.