Topolo Docs Topolo Platform
Open workspace

Upload a job board image

Store an image for the job board (a logo, the hero cover, a gallery picture or the share image) and get its public https url to use in an appearance field. PNG, JPEG or WebP only, checked against the bytes, sent as base64. Images are public once uploaded. Recruiters only.

Outcome

Store an image for the job board (a logo, the hero cover, a gallery picture or the share image) and get its public https url to use in an appearance field. PNG, JPEG or WebP only, checked against the bytes, sent as base64. Images are public once uploaded. Recruiters only.

Before you start

  • Verify identity and the pinned organization context, then discover the exact action and a granted Recruit workspace.
  • The required application permission is workspace:write. Hiring-team roles and record visibility are enforced separately; a workspace grant alone does not grant every Recruit action.
  • Organization and workspace identity come from verified context. Pass the workspace as resourceType/resourceId, separate from action input; request-body organizationId/workspaceId values cannot select an authorized scope.
  • The first authorized action can initialize an identity-only workspace mirror and invoke the configured workspace-created callback. It does not create an Auth workspace or grant new access.

Recruiters can perform the full hiring workflow. Hiring managers see their managed roles and candidates; interviewers see assigned interviews and their candidates. Workspace owners and organization administrators act as recruiters. Use team.me to read the caller's effective role and allowed actions.

Discover the target

topolo whoami --json
topolo actions get app_topolo_recruit.job_board.assets.upload --detail --json
topolo resources topolo-recruit --json
topolo workspaces list topolo-recruit --json
topolo actions get app_topolo_recruit.team.me --detail --json

Request

POST /api/job-board/assets

Save input as payload.json. CLI and MCP action input includes path identifiers. The reviewed plan separates path/query fields from the HTTP body. Set RECRUIT_WORKSPACE_ID from granted resource discovery.

Inspect and test this action in the signed action console. Use its discovered schema and plan; a test response still requires the verification described below.

Input fields

FieldRequiredTypeConstraints
filenameRequiredstring{"minLength":1,"maxLength":200}
contentTypeRequiredstring{"enum":["image/png","image/jpeg","image/webp"]}
contentBase64Requiredstring{"minLength":1,"maxLength":2796220}

Exact input schema:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "filename": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
    },
    "contentType": {
      "type": "string",
      "enum": [
        "image/png",
        "image/jpeg",
        "image/webp"
      ]
    },
    "contentBase64": {
      "type": "string",
      "minLength": 1,
      "maxLength": 2796220
    }
  },
  "required": [
    "filename",
    "contentType",
    "contentBase64"
  ],
  "additionalProperties": false
}

Effects

  • Stores the image in Recruit's document storage under the board assets prefix and records its size and dimensions.

Interpret the response

Fetch asset.url and expect 200 with the image.

The response identifier path is $.asset.id for job_board_asset. Lists may be empty; an empty list is not a discovered target.

Exact success schema:

{
  "type": "object",
  "required": [
    "asset"
  ],
  "properties": {
    "asset": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "url": {
          "type": "string"
        },
        "contentType": {
          "type": "string"
        },
        "sizeBytes": {
          "type": "integer"
        },
        "width": {
          "type": "integer"
        },
        "height": {
          "type": "integer"
        }
      }
    }
  }
}

Worked examples

Actual dispatcher and migration-created SQLite or Worker handlers; Auth/workspace identity, object storage, mail and Notify dependencies are simulated. These synthetic fixtures establish no live authorization or delivery. A synthetic fixture identifier grants no access. Validate and inspect the planned request before executing. Commands with --confirm or MCP confirm:true assume the intended effect or data export has already been authorized.

Successful request

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This complete response is a local fixture, not a deployed or delivered result.

Input · payload.json:

{
  "filename": "image",
  "contentType": "image/png",
  "contentBase64": "iVBORw0KGgoAAAANSUhEUgAABLAAAAMgCAYAAAA="
}

CLI:

topolo actions validate app_topolo_recruit.job_board.assets.upload --data @payload.json --json
topolo actions plan app_topolo_recruit.job_board.assets.upload --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_recruit.job_board.assets.upload --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --confirm --json

MCP tool and arguments:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_recruit.job_board.assets.upload",
    "input": {
      "filename": "image",
      "contentType": "image/png",
      "contentBase64": "iVBORw0KGgoAAAANSUhEUgAABLAAAAMgCAYAAAA="
    },
    "resourceType": "workspace",
    "resourceId": "<identifier returned by resource discovery>",
    "confirm": true
  }
}

Captured local response · HTTP 201:

{
  "asset": {
    "id": "bimg_8f4d08cccd1a40f8827a740b86418474",
    "contentType": "image/png",
    "sizeBytes": 29,
    "width": 1200,
    "height": 800,
    "sha256": "3bf9eed9d13c32f75237c0e0213686943b7b4a74c9e588f03c5426a17b183ca7",
    "createdAt": "2026-10-05T09:00:00.000Z",
    "url": "https://recruit.test/jobs/_assets/bimg_8f4d08cccd1a40f8827a740b86418474"
  }
}

HTTP 201. Fetch asset.url and expect 200 with the image.

Another supported request

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This complete response is a local fixture, not a deployed or delivered result.

Input · payload.json:

{
  "filename": "image",
  "contentType": "image/jpeg",
  "contentBase64": "/9j/4AAQSkZJRgABAQAAAQABAAD/wgARCAHgAoADASIAAhEBAxEB"
}

CLI:

topolo actions validate app_topolo_recruit.job_board.assets.upload --data @payload.json --json
topolo actions plan app_topolo_recruit.job_board.assets.upload --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json
topolo actions call app_topolo_recruit.job_board.assets.upload --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --confirm --json

MCP tool and arguments:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_recruit.job_board.assets.upload",
    "input": {
      "filename": "image",
      "contentType": "image/jpeg",
      "contentBase64": "/9j/4AAQSkZJRgABAQAAAQABAAD/wgARCAHgAoADASIAAhEBAxEB"
    },
    "resourceType": "workspace",
    "resourceId": "<identifier returned by resource discovery>",
    "confirm": true
  }
}

Captured local response · HTTP 201:

{
  "asset": {
    "id": "bimg_b15edb0c0a5244afbed88cfa0c9c30c4",
    "contentType": "image/jpeg",
    "sizeBytes": 39,
    "width": 640,
    "height": 480,
    "sha256": "82214cb1e1bf67f10ba67ec5dca267ff9b9066a87ea8ca42276db62a5aa07bf2",
    "createdAt": "2026-10-05T09:00:00.000Z",
    "url": "https://recruit.test/jobs/_assets/bimg_b15edb0c0a5244afbed88cfa0c9c30c4"
  }
}

HTTP 201. Fetch asset.url and expect 200 with the image.

Refused request · board_asset_type_mismatch

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.

Input · payload.json:

{
  "filename": "image",
  "contentType": "image/jpeg",
  "contentBase64": "iVBORw0KGgoAAAANSUhEUgAABLAAAAMgCAYAAAA="
}

CLI:

topolo actions validate app_topolo_recruit.job_board.assets.upload --data @payload.json --json
topolo actions plan app_topolo_recruit.job_board.assets.upload --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json

Captured local response · HTTP 422:

{
  "error": {
    "code": "board_asset_type_mismatch",
    "message": "The file's contents do not match its type. Upload a PNG, JPEG or WebP image.",
    "messageKey": "recruit.api.errors.boardAssetTypeMismatch",
    "locale": "en"
  }
}

HTTP 422. Preserve the exact response code and details. Do not claim success.

Refused request · board_asset_not_image

Actual dispatcher and migration-created SQLite; verified Auth/workspace context, object storage, mail and Notify dependencies are simulated. No live delivery or credential authorization is established. This is a refused request; diagnose it before retrying.

Input · payload.json:

{
  "filename": "image",
  "contentType": "image/png",
  "contentBase64": "JVBERi0xLjQKMSAwIG9iago8PD4+CmVuZG9iagp0cmFpbGVyCjw8Pj4KJSVFT0YK"
}

CLI:

topolo actions validate app_topolo_recruit.job_board.assets.upload --data @payload.json --json
topolo actions plan app_topolo_recruit.job_board.assets.upload --resource-type workspace --resource-id "$RECRUIT_WORKSPACE_ID" --data @payload.json --json

Captured local response · HTTP 422:

{
  "error": {
    "code": "board_asset_not_image",
    "message": "Upload a PNG, JPEG or WebP image.",
    "messageKey": "recruit.api.errors.boardAssetNotImage",
    "locale": "en"
  }
}

HTTP 422. Preserve the exact response code and details. Do not claim success.

Verify the result

  • Fetch asset.url and expect 200 with the image.

Errors and recovery

  • organization_context_required (HTTP 403): Select the organization authorized by the credential; do not send an organization id in the action input.
  • permission_denied (HTTP 403): Grant the credential the required Recruit workspace permission.
  • hiring_team_role_required (HTTP 403): Ask a recruiter to give the caller a hiring-team role that allows this action (team.members.set); error.details lists the allowed roles.
  • workspace_required (HTTP 409): Create a Recruit workspace, or pass the workspace resource context.
  • invalid_input (HTTP 422): Correct the fields named in error.details using inputSchema, then retry.
  • board_asset_too_large (HTTP 413): Send a smaller image (see the input schema).
  • board_asset_not_image (HTTP 422): Send a PNG, JPEG or WebP image.
  • board_asset_type_mismatch (HTTP 422): Set contentType to the image's real type.
  • board_asset_dimensions (HTTP 422): Send an image of at most 6000 pixels a side.
  • invalid_json (HTTP 400): Send a valid JSON object. The request is rejected before workspace resolution or record creation.
  • api_key_workspace_binding_required (HTTP 403): Create the API key bound to a workspace (an organization-wide key names a workspace nobody verified), then call again.
  • workspace_required (HTTP 400): Send the workspace the key is bound to with --resource-type workspace --resource-id <workspace id> (or the X-Topolo-Resource-Type and X-Topolo-Resource-Id headers).
  • workspace_forbidden (HTTP 403): Use a workspace the key is bound to; read the key's bindings in API key management.
  • workspace_access_insufficient (HTTP 403): Give the key the workspace permission this action needs: workspace:read to read and workspace:write to change. A delete also needs workspace:write, because the write permission is what grants the read_write level.

Authentication or gateway failures may occur before the Recruit handler. Preserve the exact discovered service error, request id and credential context. The local fixtures do not establish live credential authorization.

Recovery boundary

An uploaded image stays available; stop using its url in the appearance to stop showing it.

  • Edit the job board's draft appearance: Change the board's next appearance without changing the live board: the same appearance fields and limits as job_board.update, applied on top of the current draft (or of the live appearance when there is none). Check it with job_board.preview, then make it live with job_board.draft.publish or drop it with job_board.draft.discard. Only the fields sent change. Recruiters only.
  • Update the job board: Set the job board's slug (its unique public URL /jobs/{slug}), title (the company name candidates see), intro and appearance, live at once; job_board.draft.update edits a draft instead. appearance.source brand (default) follows the organization's published Brand; a field the board sets overrides Brand and null clears it; manual ignores Brand. Appearance holds logos, colour, font, themeMode, header and footer links, the hero (text or image variant, headline, subheadline, cover with alt text and focal point), the optional sections (about, whyJoin, benefits, gallery, hiringProcess; fixed order, each on or off), rolesList, confirmationMessage and seo. Limits are in the input schema. Only fields sent change; a list sent replaces that list. Recruiters only.

Evidence

Actual dispatcher and migration-created SQLite or Worker handlers; Auth/workspace identity, object storage, mail and Notify dependencies are simulated. These synthetic fixtures establish no live authorization or delivery. Test capture provenance is retained with each fixture. API, database and integration outcomes require separate verification in the target environment.

Documentation history

  • 2026-10-18: Added this action guide with reviewed request, response, permission, verification and recovery guidance from source 63171ed7c08aa066adb44cc01e64011e465302e1. Examples are retained local captures, not production execution or delivery proof.