GRANSKA

Get an upload URL

POSThttps://api.granska.cloud/v1/upload-url

Issues a job id and a pre-signed URL to upload a PDF to, for documents too large to inline.

Bearer tokenSpends no quota

Use this, not base64

There are three ways to give the engine a document, and this is the one to build against. The other two — pdfBase64 and pdfUrl on POST /v1/analyze — exist and work, but they make the analysis call carry the document.

That matters because POST /v1/analyze is meant to answer in milliseconds. When the document arrives inline as base64, the gateway has to receive the whole encoded string, decode it and store it before it can queue anything; when it arrives as a URL, the gateway has to fetch it first. Both turn a queue operation into a transfer, use far more memory, and put a file-sized transfer inside a request with an HTTP timeout on it. A large document is exactly where that goes wrong, and a large document is the normal case here.

The two-step flow moves the transfer off the API entirely. You ask for a URL, you PUT the file straight at cloud storage, and the analysis call then carries a job id and nothing else.

Request
curl -X POST https://api.granska.cloud/v1/upload-url \
  -H "Authorization: Bearer $TOKEN"

# Then PUT the document straight at the returned uploadUrl:
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/pdf" \
  --data-binary @utredning.pdf
Response
{
  "success": true,
  "jobId": "job_7d41c9",
  "uploadUrl": "https://storage.googleapis.com/uploads/job_7d41c9.pdf?X-Goog-Signature=..."
}

The two steps

  1. POST /v1/upload-url with no body. It answers with a jobId and an uploadUrl.
  2. PUT the file at uploadUrl with Content-Type: application/pdf. The content type is baked into the URL when it is issued, so sending a different one fails the upload.

Then call POST /v1/analyze with that jobId. Passing jobId implies pdfUploaded: true, so you do not have to send both.

The jobId is issued here and is the same id you poll on afterwards — an upload URL and its analysis share one identity from the beginning.

JOB=$(curl -s -X POST https://api.granska.cloud/v1/upload-url \
  -H "Authorization: Bearer $TOKEN")

curl -X PUT "$(echo "$JOB" | jq -r .uploadUrl)" \
  -H "Content-Type: application/pdf" \
  --data-binary @utredning.pdf

The Origin header, if a browser does the upload

The upload URL is a resumable-upload session, and cloud storage takes the allowed origin from the call that created the session — this one — rather than from the bucket's CORS configuration.

So if your server fetches the URL and hands it to a browser on a different origin, the browser's PUT fails CORS even though the URL is valid. Send the browser's origin as the Origin header on this call and it is baked into the session correctly.

A server-to-server upload never meets this: with no browser involved there is no CORS check.

ParameterDescription
Origin
string·header
The browser origin that will perform the upload. Baked into the returned URL as the only origin allowed to complete it.GCS's resumable-upload protocol takes the origin from this call, not from the bucket's CORS rules — so a server that fetches the URL and hands it to a browser on another origin gets a CORS failure on the upload.

Errors

This endpoint spends no quota and validates nothing beyond your token, so the only failures are authentication and an internal fault.

ErrorWhen
401
UNAUTHORIZED
The Authorization header is missing, is not a readable bearer token, or names no tenant.
401
TOKEN_EXPIRED
The access token was issued by this gateway and has since expired. Not probed: it needs a token older than its own lifetime.
500
INTERNAL_ERROR
An unexpected server-side failure. Not probable from outside — reaching it means something is wrong.

Send it without writing a client

If your organisation already has an account, an administrator can request an upload URL from the API tester at /admin/api-tester and see the response before writing any code.