Skip to main content
Artbucket never talks to a Git host. An integration does: a GitHub or GitLab app, a bot, a CI job. It holds the host’s credentials, reads and writes the repository, and calls these endpoints with an API key. The CLI uses the same ones. All of them are under /api/v1/brands/{slug}, with Authorization: Bearer ab_..., and answer { "data": ... }. Writing takes write on the workspace. An import that publishes also takes sharing, which a grant can switch off. The full schemas are in the API reference.

The model

For each brand kept in a repository, Artbucket stores its source: the repository, branch and folder, the commit both sides last agreed on, and the brand as of that commit (the base). Every import merges three states: the base, the brand in the app now, and the files.
  • Import at a commit (the repository changed): the base becomes the files at that commit.
  • Import without a commit (files from anywhere else, like the CLI): the base stays, so the next import still merges from what the repository last said.
  • PUT .../source with synced (the integration just committed what Artbucket exported): the base becomes those files, so nothing is merged or exported again.
  • Moving the source to another repository, branch or folder forgets the base: the next import takes the files whole.
A brand with no source, or no base yet, has nothing to merge from: an import takes the files whole.

The source

GET /api/v1/brands/acme/source
source is null for a brand that lives in Artbucket alone.

Connect a brand

A repository that already holds the brand

Then import the folder at its head commit, as below. With no base yet, the import takes the files whole and records them as the base. Make the brand first (POST /api/v1/brands with name and slug) when Artbucket doesn’t have it: the import names it from brand.yaml.

An empty repository, from a brand Artbucket has

  1. Export with every asset given a path: POST .../files/export with { "assets": "files" }.
  2. Commit files, and each asset in assets, fetched from its url with the key, at its path.
  3. Record it: PUT .../source with the repository and synced, the commit and what you committed.
PUT /api/v1/brands/acme/source
synced is checked like an import: files that don’t check are refused with their problems.

The repository changed

On a push to the source’s branch that touches its folder, send the folder at that commit:
POST /api/v1/brands/acme/files/import
200
Here color.primary was changed on both sides since the base: the files’ value won, and the app’s is in the version before. A conflict’s what names its piece: name, theme.radius, rule color.primary, rule color.primary (dark-background), page logo. When two sound sides make an unsound whole (a page in the app binding a rule the files removed), the import takes the files whole and says so as a conflict on brand.

The brand changed in the app

When source.pending is true, export, giving the repository’s files at its head:
POST /api/v1/brands/acme/files/export
200
files is the whole brand. A file that says the same as its previous comes back exactly as it was, comments and all, so:
  • to write: each file whose text differs from previous, or that previous lacks;
  • to delete: each file of previous under rules/ or pages/ that files no longer has;
  • assets: each path in assets whose sha256 the repository’s file doesn’t match, fetched from url with the key. assets: "files" in the request gives every asset the brand points at a path, including ones the repository never held.
Commit them, directly or as a pull request, then record the commit as agreed: PUT .../source with synced: { commit, files, assets }, the folder as committed. Until then the source stays pending. If the commit goes through a pull request that people change before merging, skip synced: the merge comes back as an import, and the base moves then.

Pull request previews

POST /api/v1/brands/acme/previews
200
  • The files are checked as an import is, and refused the same way.
  • One preview per ref: saving it again (a new push to the pull request) keeps its link and replaces what it shows.
  • diff is what merging would change of the brand as it stands, for the pull request’s comment.
  • The link needs no account, like a share link, and shows every page that isn’t hidden, members’ pages too, with files signed for whoever opens it. Treat it as you would the pull request.
  • It closes 30 days after its last update. Close it sooner when the pull request is merged or closed: DELETE .../previews?ref=pull/12.
GET /api/v1/previews/{token}?page=color answers a page of the preview as data, for an integration that draws its own.

Assets

A path under assets/ is matched to the library by the SHA-256 of its bytes (or an asset id given in its place). When a file the folder points at isn’t in the library, the request is refused with missing:
422
Upload each, then send the same request again:
A font’s type is read from its bytes when it is filed, whatever media type the upload gave. Files under assets/ no file points at can be sent or left out.

Problems

Files that don’t check are refused with 422, every problem at its file and line, and the files to upload:
422
A check on the host (a GitHub check run, a GitLab code quality report) can put each at its line, under the folder’s path. The messages are listed in the brand folder.

Connecting from the app

Set GIT_CONNECT_URL on the server to where your integration starts, with {brand} for the brand’s slug: https://git.example.com/connect?brand={brand}. New brand, a new brand’s setup and the builder link there for workspace admins, with {brand} empty when the brand is to come from a repository. When it is done, send the person back to the brand’s Overview, {APP_URL}/brands/{slug}?git=connected, which says when the first sync has landed.

A minimal integration

Import on every push to the main branch, from a CI job or a webhook handler. It assumes the brand’s source is set (PUT .../source, once).
sync.mjs
The rest of a full integration is the other direction (export when the source is pending, commit, PUT .../source with synced) and previews on pull requests, each one call as above.