SG Ground Truth

Findings

One finding answers one question and ends in a single actionable sentence. Each names the probe that produced it and quotes the status code and error string verbatim.

They are grouped by the phase of a session they bite in, which is the order a client meets them. The number is still the probe that produced it. The field types, entity types, endpoints and filters are the other half: complete, and addressed by name.

Only findings marked scope: api are published here. A finding that measures one site, or one project inside it, stays in the repository and is excluded from this build. Why that distinction exists.

Authgetting a token, and what it is

  • 001 auth

    Send the token request as application/x-www-form-urlencoded: application/json is 400 Invalid JSON body. client_credentials returns a 600s bearer, so ignore the refresh_token and re-auth.

    • auth
    • client
    • token
  • 027 auth permissions

    The token endpoint accepts password and session_token. Impersonation is the OAuth2 scope sudo_as_login:<login>, never a body field, and a lower level reads far fewer rows and as many fields.

    • auth
    • token
    • permission
    • user
    • client
    • sudo
  • 052 app session launcher partial

    Post appName and machineId, open url in a browser, PUT the id until approved. The sessionToken spends at grant_type=session_token as that person, and every mint renews the session.

    Not measured. a person clicking deny, and a session left idle past the site's expiry window; both wait on time and on the site, not on the probe

    • auth
    • token
    • user
    • launcher
    • permission

Protocolheaders, and what a status code is worth

  • 004 array vs hash

    api3_array/api3_hash are a POST _search request Content-Type, not a GET Accept header: as Accept they 406, and entity fields are returned under relationships either way.

    • query
    • header
    • entity-field
    • error-handling
    • trap
  • 028 loud and silent

    A 400 is trustworthy and usually names the legal set, but a 200 proves nothing: an unknown field, sort key or query param is a no-op, and a batch can return an id for a row it never made.

    • query
    • filter
    • sort
    • write
    • operator
    • error-handling
    • trap
    • silent
  • 051 api version

    /api/v1 and /api/v1.1 are the same API. Across 20 read-only calls the only difference is api_version in the root document and the prefix each echoes in its own links. Any other segment is 404.

    • discovery
    • client
    • paging
    • protocol
  • 062 cors partial

    Every path under /api/v1 answers the preflight and echoes any Origin, credentials true. /internal_api and the web paths send no CORS header, so a page on another origin proxies those.

    Not measured. what a live session cookie authenticates on /api/v1, since allow-credentials invites one, and the presigned upload host, which is not this site. A live session needs a person at a browser

    • cors
    • browser
    • header
    • launcher
    • client

Schemawhat the site has, and adding to it

  • 002 schema

    Fetch /schema once for the type list, then /schema/<Type>/fields only for types you actually need: it is the expensive call (48KB, ~330ms each) and must never be looped over all types.

    • schema
    • cost
    • discovery
  • 008 custom entities

    Presence in /schema is the enablement test for a custom entity: a slot absent from the listing 404s. Slot numbers are non-contiguous and site-specific, so read name.value and never hardcode one.

    • schema
    • custom-entity
    • discovery
  • 009 status lists

    A project's usable statuses are valid_values minus hidden_values, read with project_id: valid_values is identical at every scope, hidden_values is the only thing that varies.

    • schema
    • status
    • list-field
    • inspector
  • 019 create fields

    Custom fields are creatable over REST, but you pass a display name and a duplicate silently becomes <name>_1: an idempotent ensure() must read /schema first, never POST-and-hope.

    • schema
    • write
    • custom-field
    • provenance
    • entity-field
    • trap
    • silent
  • 040 field revive

    A trashed field is revived by POST /schema/<Type>/fields/<name> with {"revive": true} at 204, but it returns at its original data_type, and a PUT changing data_type is a 200 that does nothing.

    • schema
    • custom-field
    • create
    • error-handling
    • trap
    • discovery
    • silent
  • 042 spec coverage

    GET /spec.json returns the deployment's own OpenAPI v3 document. It advertises 62 operations against the 23 this corpus covers, and it disagrees with the published documentation.

    • schema
    • discovery
    • cost
  • 047 site facts and the working week

    Three site-fact calls, three different envelopes: {data, status}, a bare hash, and JSON:API. Only /schedule/work_day_rules reports a bad scope id, and it reports it as a 200.

    • discovery
    • silent
    • user
    • date
    • error-handling
  • 056 stock vs custom field

    A field with visible.editable false is stock and safe to depend on; true means the site can hide it, which is every custom field and a few stock ones. The sg_ prefix decides nothing.

    • schema
    • custom-field
    • discovery
  • 061 shipped statuses

    Nothing in the schema marks a shipped Status. system is true on a minority of them; the stock set is created_by is null, plus options[return_only]=retired for the rows a site retired.

    • status
    • icon
    • discovery
    • enumeration

Readgetting rows back

  • 003 query

    A dotted ?fields path comes back flat under literal key "sg_task.Task.content" in attributes; an entity field is returned under relationships as {data, links}. Never read a row from attributes alone.

    • query
    • filter
    • dotted-field
    • paging
    • version
  • 006 pagination

    links.next is emitted on every page forever, including zero-row ones, so stop paging when data is empty and never on a missing next.

    • paging
    • query
    • enumeration
  • 018 project listing

    sg_status is not a liveness filter and is null on 15 of 22 projects; is_template, is_demo and archived are the discriminators, so pick the ones your list wants - is_demo hides the demo show.

    • project
    • query
    • filter
    • inspector
    • list-field
    • trap
  • 021 media resolution

    PublishedFile.path is returned with the LocalStorage join already done, so a client never reads LocalStorage or reassembles a root, but a platform whose storage root is unset reads null.

    • version
    • media
    • published-file
    • path
    • storage
    • inspector
    • query
  • 023 pages

    A page's layout is the PageSetting row whose user is null; settings_json reads back as decoded JSON and body/list_content settings.columns is the column list. Every filter on it is ignored.

    • page
    • query
    • filter
    • schema
    • project
    • inspector
    • trap
    • silent
  • 026 result order

    Rows come back id ascending unless you sort; ["id", "in", [...]] discards the order of the list, and an unsortable or unknown sort field is a silent 200 no-op where the same name in a filter 400s.

    • query
    • sort
    • paging
    • filter
    • dotted-field
    • trap
    • silent
  • 048 one record beyond crud

    POST on one record is revive, not update: ?revive=1 is required and the body is ignored. /<field> reads image and attachment fields only, and relationships/<field> is the same data, unpaged.

    • page
    • entity-field
    • multi-entity
    • attachment
    • cost
    • discovery
    • silent
  • 059 dotted path type check partial

    The middle segment of a dotted path is checked against the field's valid_types in a projection and against the schema alone in a filter: the projection drops the key at 200, the filter 400s.

    Not measured. whether a filter through a type outside valid_types is evaluated or matches nothing; no row on a read-only project links one, so it needs a write

    • dotted-field
    • entity-field
    • error-handling
    • trap
    • silent
  • 060 entity dict name

    The name in an entity dict is the target's cached_display_name, filled on every type measured, single and multi alike. Read it, not the per-type identity field, and expect decoration.

    • entity-field
    • multi-entity
    • link
    • destructive
  • 064 hierarchy expand buckets partial

    Dedupe children by path and keep the first. The __none__ bucket is repeated once per group, byte-identical every time, and its rows are disjoint from every group's.

    Not measured. how the web interface draws the repeated node. Its tree needs a session a person approves (probe 052), and the saved token had expired

    • path
    • project
    • sequence
    • trap

Filterselecting the rows you want

  • 016 dotted multi entity

    A dotted path through a multi_entity field reads back nothing: HTTP 200 with the key silently absent from attributes. Filters on that same path work, including two hops.

    • query
    • dotted-field
    • multi-entity
    • filter
    • paging
    • trap
    • silent
  • 017 filter operators

    is/is_not/contains/not_contains/starts_with/ends_with/in/not_in all work, on text fields and through dotted paths; an unknown operator 400s on all 21 data types, naming the valid list on 16.

    • query
    • filter
    • operator
    • dotted-field
    • entity-field
    • error-handling
    • silent
  • 020 summarize

    _summarize needs the same vendor Content-Type as _search, and one grouping call returns a field's distinct-value count and its empty count. At ~300ms a field, rank a shortlist, never scan.

    • query
    • inspector
    • fill-rate
    • schema
    • cost
    • list-field
  • 030 complex filters

    api3_hash nests and/or groups 265 deep and mixes leaves with sub-groups; api3_array cannot express or, query-string filter[] is ignored on _search, and {path,relation,values} runs nowhere.

    • query
    • filter
    • operator
    • header
    • page
    • error-handling
    • trap
    • silent
  • 046 search without a path

    /hierarchy/_expand and /hierarchy/_search refuse the vendor content types every other POST requires and take application/json alone, so one shared POST helper 415s on half the API.

    • query
    • filter
    • header
    • project
    • trap
  • 053 text search matching partial

    page.size caps at 25 and defaults to 25 with no links, so page with page.number. Every word must match a case-insensitive substring of the name or of the linked row's name.

    Not measured. Only `description` and a linked row's name were tried as fields beyond the name, and the tie-break between equal-length names was not reached. The probe's limit, not the site's.

    • query
    • paging
    • cost
    • link
    • silent
  • 063 text search filter shape

    An entity_types value follows the request Content-Type: an array of triples under api3_array, a logical_operator group under api3_hash, which alone nests. The other shape is 400 code 103.

    • header
    • error-handling
    • trap
  • 068 note read state

    read_by_current_user is per person and missing from the schema; is and is_not are evaluated, while in, not_in and an unknown is value all return the unread rows at 200.

    • note
    • list-field
    • user
    • permission
    • silent
    • trap
  • 071 note link name filter

    Filter notes about a thing on note_links.<Type>.cached_display_name: it resolves for every valid type, code 400s on Booking and name on all but Department. The path cannot be read back.

    • note
    • dotted-field
    • multi-entity
    • link

Writecreating and updating

  • 011 create project

    A script user can create a Project with nothing but {"name": ...}, at 201, but the response echoes only 6 attributes, so read the project back if you need anything else.

    • write
    • project
    • create
    • schema
  • 012 create version

    The schema's mandatory flags are not the create contract: on every project-scoped type measured, project is required and the identity field is optional, server-generated and not unique.

    • write
    • version
    • create
    • entity-field
  • 024 read after write

    Every write ignores ?fields. A create returns what you sent plus the server defaults, an update returns the whole record, and neither resolves a dotted path, so re-read for those and after an upload.

    • write
    • create
    • batch
    • upload
    • async
    • entity-field
    • trap
    • silent
  • 045 webhooks partial

    The hook contract validates the url and the entity type, and silently accepts a field name, a project id and an entity-type count it will never honour. test_connection answers 204 for any uuid.

    Not measured. No entity event reached a hook on the probed site, so the delivery payload, the `X-SG-SIGNATURE` and `x-sg-event-batch-*` headers, and `batch_deliveries`, are all unrecorded.

    • webhook
    • silent
    • trap
    • create
    • write
    • delivery
    • error-handling
    • token
  • 050 webhook subscriptions

    entity_types and event_type are mutually exclusive and one 400 covers giving neither and giving both. revive is a fourth action, and every entity the guide calls excluded is accepted at 201.

    • webhook
    • silent
    • trap
    • create
    • error-handling
    • enumeration
  • 058 local storage roots

    One create fills every local_path_* the storage row defines, whichever platform's root the path was under. The server picks the deepest matching root, and no conditional-write header is honoured.

    • path
    • storage
    • published-file
    • etag
    • silent
  • 069 client note

    client_note cannot be set over REST: true on create is 400 and any PUT is 400 editable on create only. sg_note_type: "Client" is the one marker a caller can write.

    • note
    • list-field
    • create
    • sudo
  • 070 authored timestamps partial

    A create body sets created_at and updated_at and they read back exactly, on Note, Task and Version, though the schema flags both editable false; every PUT on either 400s.

    Not measured. Whether an EventLogEntry create can date itself is untried: that row cannot be deleted afterwards (probe 025), so the probe did not spend one.

    • date
    • create
    • sudo
    • trap

Uploadgetting bytes in and out

  • 013 upload media

    Media upload is three calls: GET {field}/_upload, PUT the bytes, POST links.complete_upload. Transcoding is async: poll until the field stops reading /images/status/transient/.

    • write
    • upload
    • media
    • attachment
    • version
    • async
  • 014 attach file

    Leave the field out of the _upload path and the file is stored as an Attachment on attachment_links; read it back with POST /entity/attachments/_search, never flat filter[].

    • write
    • upload
    • attachment
    • provenance
    • version
    • multi-entity
    • filter
    • header
  • 022 sequence on version

    sg_uploaded_movie is single-valued, and replacing it leaves sg_uploaded_movie_mp4 describing the old file while status reads 1. A sequence belongs in sg_path_to_frames.

    • version
    • media
    • upload
    • sequence
    • path
    • attachment
    • write
  • 039 upload silent failures

    complete_upload returns 201 and creates an Attachment even when the bytes were never PUT, and file_size is null on a good upload too, so only fetching the stored file proves it exists.

    • media
    • attachment
    • upload
    • error-handling
    • trap
    • note
    • silent
  • 044 multipart upload

    multipart_upload=true on the init sets upload_id and adds links.get_next_part. Every part but the last must be at least 5 MiB, and completion needs an etags array inside upload_info.

    • multipart
    • upload
    • etag
    • storage

Observewhat changed

  • 025 event log partial

    meta.old_value and meta.new_value answer "what was this before", but meta is unfilterable and unsortable: narrow on entity, event_type and attribute_name, sort -id, read meta yourself.

    Not measured. Whether event_type or meta can be set in the create body is untried: testing it costs another permanent row. Every event read was generated by another user; probe 049 covers the script's own.

    • event-log
    • query
    • filter
    • operator
    • paging
    • write
    • create
    • serializable
    • status
    • trap
    • silent
  • 043 attention

    The six attention calls share no convention with the rest of the API: no paging, no fields, links.self spelled /entity/Shot/7668, and a missing record id on activity_stream is a 500.

    • follow
    • user
    • note
    • reply
    • paging
    • header
    • async
    • trap
    • silent
  • 049 script events

    A script's writes reach the event log only while its ApiUser has generate_event_log_entries True. The default is False and nothing errors when off. One create logs one row per field plus one _New.

    • event-log
    • permission
    • write
    • silent
    • trap
    • observe
    • auth
  • 066 user feed

    A HumanUser's activity_stream is what the person created, not what they follow: 0 of 9 rows touched the 81 followed records. A feed is a fan-out over their tasks' Shots and Assets.

    • follow
    • user
    • task
    • version
    • observe
  • 067 notes in the stream

    A Reply reaches every linked stream in 33 s as create_reply, creates too; a script's Note create and status changes were absent after 430 s. Write as a person.

    • note
    • reply
    • task
    • version
    • async
    • silent

Rendershowing it to a person

  • 010 status icons

    Status.icon is an entity link under relationships; display_type picks one of three renderings; url is empty unless image_data is asked for beside it; the stock sprite is in the site's own stylesheet.

    • status
    • icon
    • cache
    • colour
    • entity-field

Unphased

A finding answers a question. A recipe is a call that ran, with the response it returned.

4 behaviours the REST documentation does not describe

Each is a recorded probe result. The entry that measured it is linked below it.

  • An unknown filter operator returns 400 naming every legal relation.

    Measured on all 21 reachable data types: every one 400s, and 16 of them enumerate the field's whole legal vocabulary in the error. The other five take no operator at all and say so. A filter typo can never pass as "no filter".

    Valid relations: ["is", "is_not", "greater_than", "less_than",
     "in_last", "not_in_last", "in_next", "not_in_next",
     "in_calendar_week", "in_calendar_month", "in_calendar_day",
     "in_calendar_year", "between", "in", "not_in"]

    filter operators

  • An unknown ?fields name is dropped at 200.

    The opposite of the filter path. A misspelled field is removed from the response and the request succeeds, so the row comes back missing a key rather than reporting an error.

    GET /entity/versions?fields=sg_not_a_field
    -> 200, n=1

    query

  • A write can be accepted at 200 and discarded.

    Task.splits takes a well-formed array of hashes, answers 200, and stores null. A client that trusts the status code records a value that is not there.

    PUT [{"start_date": "2026-01-01", "duration": 480}]
    -> 200      reads back: null

    serializable

  • hidden_values is not enforced on write.

    REST accepts a status the project hides from its own UI, and reads it back. Only valid_values is enforced, so every client must subtract hidden_values itself.

    usable = [v for v in valid_values if v not in hidden_values]

    status_list

Every entry on this site is the output of a probe in probes/. The corpus is generated by running those probes against a live Flow Production Tracking site, not written from memory.

Not affiliated with or endorsed by Autodesk. Flow Production Tracking is their product; this is an independent record of how its REST API answers.