Design

A design is the white-label theme applied to your team's public passports — the pages a QR code resolves to. Colors, corner rounding, shadow depth, logo and font: everything a visitor sees that is yours rather than Synex's.

There is exactly one design per team, and it is addressed without an id. A key is pinned to one team and a team has one theme, so there is nothing for an id to disambiguate — /v1/design is the whole address.

The abilities are the whole gate

Neither design:read nor design:write is in a key's default ability set — grant them explicitly when you mint the key.

They are non-default for a reason worth stating plainly: a key minted to write passport values has no business restyling every public passport the team has ever published. One careless grant turns a data integration into something that can repaint the customer-facing surface of the whole product line.

There is deliberately no role gate on top of that. Any team member may edit branding in the Synex application, so requiring admin or owner here would make the API stricter than the surface it mirrors — and a rule that only exists on one of two equivalent paths is not a security boundary, just a difference to work around.

The design model

  • Name
    object
    Type
    string
    Description

    Always design.

  • Name
    palette
    Type
    array
    Description

    An ordered color scale of { lightness, hex } entries. [] when unset. At most 32 entries; the application writes 11.

  • Name
    rounding
    Type
    integer
    Description

    Corner-rounding level, 1–6. Nullable.

  • Name
    shadow
    Type
    integer
    Description

    Shadow-depth level, 1–6. Nullable.

  • Name
    logo
    Type
    string
    Description

    A file_ id from your team's Drive. Nullable.

  • Name
    font
    Type
    string
    Description

    A font family name. Nullable.

palette is an ordered scale, not a bag of settings

The array's position carries meaning. The renderer reads index 5 as the brand color and index 0 as the page background; the rest fill in the shades between. Reordering the entries restyles the passport even though every color in it is unchanged, so treat the palette as a scale you author top to bottom rather than as a set of named options you can shuffle.

Each entry is {lightness, hex}lightness on the familiar 50/100/200/…/900/950 scale, and hex as #RRGGBB.

rounding and shadow are levels, not measurements

Both are integers from 1 to 6, and both index the renderer's own class maps. rounding: 3 is not "3px" and shadow: 2 is not a blur radius — they select the third and second steps of scales the renderer defines. Move them a step at a time and look at the result; there is no unit to compute with.

An unset design reads as null, not as a default

A team that has never set a theme gets palette: [] and null for every scalar. Those nulls mean "not set", not "set to the default". The renderer's own fallbacks decide what an unthemed passport looks like, and reporting those fallbacks here would claim the team had picked something it never picked — which is exactly the claim that breaks the moment the renderer's defaults change.

If you are mirroring a design into your own system, carry the nulls through rather than substituting values for them.

logo is an id, not a link

GET /v1/design returns the logo's file id and stops there. To get the bytes, turn it into a link yourself with GET /v1/files/{file}/download.

A presigned URL is a bearer credential with a clock on it. Minting one on every read of the theme would hand you a signature you did not ask for, on every poll, that you could not cache for longer than its own short lifetime — and it would put a signing round-trip inside a call that is otherwise a single cheap read.


GET/v1/design

Retrieve the design

Reads the team's theme. Requires design:read.

The singleton always exists — a team that has never touched its branding still has a design, one whose every field is null — so this never 404s.

Request

GET
/v1/design
curl https://api.synexcloud.com/v1/design \
  -H "Authorization: Bearer $SYNEX_API_KEY"

Response

{
  "object": "design",
  "palette": [
    { "lightness": 50, "hex": "#F5F8FC" },
    { "lightness": 100, "hex": "#E7EEF8" },
    { "lightness": 200, "hex": "#C8D9F1" },
    { "lightness": 300, "hex": "#9CBCE6" },
    { "lightness": 400, "hex": "#6698DA" },
    { "lightness": 500, "hex": "#2F6FED" },
    { "lightness": 600, "hex": "#2459C4" },
    { "lightness": 700, "hex": "#1C459B" },
    { "lightness": 800, "hex": "#153472" },
    { "lightness": 900, "hex": "#0E2450" },
    { "lightness": 950, "hex": "#0A1836" }
  ],
  "rounding": 3,
  "shadow": 2,
  "logo": "file_00a1b2c3d4e5f607",
  "font": "Inter"
}

Unset

{
  "object": "design",
  "palette": [],
  "rounding": null,
  "shadow": null,
  "logo": null,
  "font": null
}

PATCH/v1/design

Update the design

Moves only the fields present in the body. Requires design:write. Omit a field and it keeps its stored value; there is no need to read the theme, edit it, and send it back whole.

palette is the exception: it replaces wholesale. Merging a caller's three entries into a stored eleven would splice two scales together and produce something nobody designed — a brand color from one palette sitting on a background from another. Send the entire scale, or leave the key out of the body entirely.

Clearing a field

logo and font accept an explicit null to unset them.

The logo must exist

A logo that does not name a file in this team's Drive is rejected with 400 resource_missing and param: "logo". Nothing is written.

It is checked because the failure it prevents is silent and public. A logo pointing at nothing renders as a broken image on every public passport the team publishes, and the write that caused it would have returned a 200 that looked exactly like success.

It is a 400 rather than a 404 because of what the request addressed: the /v1/design resource exists and was found; it is the logo parameter that names a missing object.

An empty body is an error

PATCH /v1/design with {} is 400 parameter_invalid, not a no-op success. A body naming no field is a caller mistake — a serializer that dropped its fields, a config object that came back empty — and a 200 would hide it behind a response that looks exactly like the change landing.

It also keeps the change feed honest: no design.updated is emitted for a request that changed nothing.

Optional attributes

  • Name
    palette
    Type
    array
    Description

    The whole color scale. Replaces, never merges.

  • Name
    rounding
    Type
    integer
    Description

    Corner-rounding level, 1–6.

  • Name
    shadow
    Type
    integer
    Description

    Shadow-depth level, 1–6.

  • Name
    logo
    Type
    string
    Description

    A file_ id from your team's Drive, or null to clear.

  • Name
    font
    Type
    string
    Description

    A font family name, or null to clear.

Request

PATCH
/v1/design
curl -X PATCH https://api.synexcloud.com/v1/design \
  -H "Authorization: Bearer $SYNEX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "palette": [
      {"lightness": 50,  "hex": "#F5F8FC"},
      {"lightness": 500, "hex": "#2F6FED"},
      {"lightness": 950, "hex": "#0A1836"}
    ],
    "rounding": 3
  }'

Response

{
  "object": "design",
  "palette": [
    { "lightness": 50, "hex": "#F5F8FC" },
    { "lightness": 500, "hex": "#2F6FED" },
    { "lightness": 950, "hex": "#0A1836" }
  ],
  "rounding": 3,
  "shadow": 2,
  "logo": "file_00a1b2c3d4e5f607",
  "font": "Inter"
}

shadow, logo and font were absent from that body, so they kept whatever they held. The palette, being a palette, replaced the stored one entirely.

Events

A successful PATCH emits design.updated. Its payload is byte-identical to the response you just received, so a consumer on the change feed sees the same theme the caller does, with no follow-up read.

There is no design.created. The singleton always exists, so there is no moment of creation to report.

Was this page helpful?