API
Artifacts
Files an agent can read. Uploading is a two-call handshake because the bytes go straight to storage and never through this platform; reading one back is a short-lived URL for the same reason.
A scope is a filename prefix
project:policy.pdf, agent:rates.csv, user:scan.pdf, or a bare scan.pdf for this conversation only. The prefixes are the agent framework's own idiom, so the same name works in your tool code via load_artifact(filename). A name that comes back from the listing KEEPS its prefix — hand back a bare policy.pdf and it resolves to a different file, or to nothing.
The two shared scopes are admin-only to write
project: and agent: are a tenant's reference material, readable by an agent and written by a tenant admin. A non-admin write is a 403 naming the scope.
A ticket cannot reach any of this
An end user's ticket opens the event stream and nothing else; every route here refuses one with 403. A browser uploading for one of your users gets a URL your backend declared with its own credential, naming them in user, and PUTs the bytes there.
Always send project
Empty means the platform's own project, not yours: a project-scoped credential is refused with 403, and any other addresses a bucket that holds none of your files. Both SDKs fill it in from the client's project.
user is one of your end users
Your own id for them — ada or eu:ada, read the same way. Any value is taken as an end user, so a colleague's files cannot be named here; omitted, the files are the caller's own. Any member of the tenant may name any of your end users.
The samples use $API and $TOKEN, which the API overview sets.
Declare an upload
POST/v1/artifacts/uploadsrt-service
Authorizes one upload and hands back a URL to PUT the bytes to, with the exact headers to send.
Send the headers back exactly as given, and send nothing else
The signature covers them. One header out of place, or one added, and storage refuses the PUT — which is the main reason to let the SDK do this when your own process holds the bytes.
Never send your platform token to the returned URL
It is not on this platform. The signature IS the authorization, so an Authorization header there would put a credential somewhere it has no business being.
The upload IS the artifact
The object lands exactly where the agent framework would have written it, so there is nothing to register afterwards: your tool finds it with load_artifact(filename) and the agent can discover it by listing. Saving the same filename again creates a new version rather than overwriting.
# 1. Declare it.
UPLOAD=$(curl -s -X POST "$API/v1/artifacts/uploads" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"target_slug": "hotel-concierge", "project": "agents",
"session_id": "sess-ada-1", "filename": "scan.pdf",
"content_type": "application/pdf", "size": 18342}')
# 2. PUT the bytes, with exactly the headers it gave you.
curl -X PUT "$(echo "$UPLOAD" | jq -r .url)" \
-H "Content-Type: application/pdf" \
-H "x-goog-content-length-range: 18342,18342" \
--data-binary @scan.pdfBody
| Field | Type | Description | |
|---|---|---|---|
| target_slug | string | required | The target whose store this belongs to. |
| session_id | string | required | The conversation. It is part of the address for the session scope. |
| filename | string | required | With a scope prefix, or bare for this conversation only. |
| content_type | string | required | The real one. It is what the model is told the file is. |
| size | integer | required | Exactly how many bytes will be PUT. Up to 512 MiB. This is pinned into the signature, so it must be exact rather than an upper bound. |
| project | string | optional | The project the target is in. Send it: empty is the platform's own. |
| user | string | optional | One of your end users, whose file this is. Omitted, the caller's own. |
Responses
| Status | Meaning |
|---|---|
| 201 | Authorized. Carries the URL, the method, the headers and an expiry. |
| 401 | No credential, or one that does not verify. |
| 403 | A ticket; a non-admin writing the project or agent scope; user=*; or a project the credential is not scoped to, an empty one included. |
| 422 | The filename or the size was refused — over 512 MiB, or zero — or an unknown field, naming the field you supplied rather than a storage path. |
| 501 | This deployment has no artifact transfer configured. |
Response body
{
"url": "https://storage.googleapis.com/...&X-Goog-Signature=...",
"method": "PUT",
"headers": {
"Content-Type": "application/pdf",
"x-goog-content-length-range": "18342,18342"
},
"expires_at": "2026-10-17T09:29:02Z",
"max_bytes": 18342,
"filename": "scan.pdf",
"version": 0
}List what a conversation can see
GET/v1/artifactsrt-service
Every filename reachable from one session, across all four scopes, with the prefixes intact.
Omit session_id for the shared scopes
The project and agent scopes have no session, so a listing of them cannot require one. Pass it to see a conversation's own files as well — which is what you want before an invocation, and not what you want when rendering a tenant's document library.
curl "$API/v1/artifacts?target_slug=hotel-concierge\ &project=agents&session_id=sess-ada-1" \ -H "Authorization: Bearer $TOKEN"
Parameters
| Field | Type | Description | |
|---|---|---|---|
| target_slug | query | required | The target whose store to read. |
| session_id | query | optional | Include this conversation's own files. Omit for the shared scopes only. |
| project | query | optional | The project the target is in. Send it: empty is the platform's own. |
| user | query | optional | One of your end users, whose files to include. Omitted, the caller's own. |
Responses
| Status | Meaning |
|---|---|
| 200 | {artifacts} — a list of names. |
| 401 | No credential, or one that does not verify. |
| 403 | A ticket, user=*, or a project the credential is not scoped to, an empty one included. |
| 422 | No target_slug. |
| 501 | This deployment has no artifact transfer configured. |
Response body
{
"artifacts": [
"project:refund-policy.pdf",
"agent:rates-2026.csv",
"user:passport.jpg",
"scan.pdf"
]
}Get a download URL
GET/v1/artifacts/downloadrt-service
A short-lived URL for one file. A browser can navigate straight to it.
A URL rather than the bytes, and it needs no CORS
Nothing reads the response from script, so navigating or setting it as an href works with no cross-origin configuration at all.
Omitting version means the newest
Which is what saving the same filename twice produces. Pin one only if you kept the number — a retention rule deletes old versions before the newest, so a pinned old version can legitimately disappear while the file still exists.
curl "$API/v1/artifacts/download?target_slug=hotel-concierge\ &project=agents&session_id=sess-ada-1&filename=scan.pdf" \ -H "Authorization: Bearer $TOKEN"
Parameters
| Field | Type | Description | |
|---|---|---|---|
| target_slug | query | required | The target whose store to read. |
| session_id | query | required | The conversation. |
| filename | query | required | With its scope prefix, as the listing gave it. |
| version | query | optional | A specific version. Omitted, the newest. |
| attachment | query | optional | true by default — the browser saves it. false lets it render inline. |
| project | query | optional | The project the target is in. Send it: empty is the platform's own. |
| user | query | optional | One of your end users, whose file it is. Omitted, the caller's own. |
Responses
| Status | Meaning |
|---|---|
| 200 | {url, filename, scope}. |
| 401 | No credential, or one that does not verify. |
| 403 | A ticket, user=*, or a project the credential is not scoped to, an empty one included. |
| 404 | No artifact by that name. |
| 422 | The filename was refused — most often a prefix that is not a scope — or target_slug, session_id or filename is missing. |
| 501 | This deployment has no artifact transfer configured. |
Delete a file
DELETE/v1/artifactsrt-service
Removes one artifact and every version of it. There is no undo.
The one operation with no signed URL
This service can act as the signer directly, and a signed DELETE would put a destructive credential in a browser. The shared scopes are admin-only to delete from; a conversation's own files and a user's can be deleted by any member of the tenant.
curl -X DELETE "$API/v1/artifacts?target_slug=hotel-concierge\ &project=agents&session_id=sess-ada-1&filename=scan.pdf" \ -H "Authorization: Bearer $TOKEN"
Parameters
| Field | Type | Description | |
|---|---|---|---|
| target_slug | query | required | The target whose store to write. |
| filename | query | required | With its scope prefix. |
| session_id | query | optional | Required for a session-scoped file. |
| project | query | optional | The project the target is in. Send it: empty is the platform's own. |
| user | query | optional | One of your end users, whose file it is. Omitted, the caller's own. |
Responses
| Status | Meaning |
|---|---|
| 204 | Gone. |
| 401 | No credential, or one that does not verify. |
| 403 | A ticket; a non-admin deleting from the project or agent scope; user=*; or a project the credential is not scoped to, an empty one included. |
| 422 | The filename was refused. |
| 501 | This deployment has no artifact transfer configured. |
Response body
204 — no body. There is deliberately nothing to read back.