Currency

Portfolio

A portfolio item is one piece of past work shown on your storefront's credibility strip: a project you shipped, who it was for, and where to see it. Portfolio is a Storlaunch built-in — it is not a module, so it needs nothing enabled and is available on every plan.

Items belong to exactly one workspace (accountId) and render in curated order (sortOrder ascending, then oldest first). Each may optionally point at one of your own products via productId, which links the case study to the thing it sells.

All requests on this page require an sk_* key. See Authentication for the bearer scheme, and API overview for the response envelope.

Endpoints

Method Path Purpose
POST /v1/portfolio Create an item
GET /v1/portfolio List all items
GET /v1/portfolio/:id Retrieve one item
PATCH /v1/portfolio/:id Update an item (including reordering)
DELETE /v1/portfolio/:id Delete an item permanently

See also Testimonials and Certifications — the other two credibility resources, which share these conventions exactly.

Create an item

POST /v1/portfolio

title is the only required field.

New items are published by default. Unlike products (which default to published: false), a portfolio item is live on your public storefront the moment you create it. Pass "published": false if you want to stage it first.

Request body

Field Type Required Description
title string (1–140) yes The project name. Trimmed.
description string (≤5000) no What was done, for whom, the outcome. Defaults to "".
images array of strings (≤8) no Gallery image URLs. Each must be an http(s):// URL or a root-relative path such as /api/v1/files/<name>blob: and data: previews are rejected. Defaults to [].
productId string | null no One of your own product ids. A product belonging to another workspace is rejected, not silently dropped.
externalUrl string | null no Link to the live project or case study. https:// onlyhttp:// and relative paths are rejected, because this link leaves your storefront. Max 2048.
clientName string (≤140) | null no Who the work was for.
completedAt string (ISO 8601) | null no When the project wrapped. Rendered as the year on the card.
published boolean no Visible on the public storefront. Default true.
sortOrder integer (≥0) no Position in the strip, ascending. Default 0; ties break oldest-first.

Passing "" or null for externalUrl or clientName clears the column.

Response201 Created

{
  "data": {
    "item": {
      "id": "pfi_01HXAB7K3M9N2P5QRS8TVWXY3Z",
      "accountId": "acc_01HX...",
      "title": "Kopi Anara rebrand",
      "description": "Full identity refresh and a new packaging system.",
      "images": ["/api/v1/files/anara-01.jpg"],
      "productId": null,
      "externalUrl": "https://kopianara.id",
      "clientName": "Kopi Anara",
      "completedAt": "2026-03-01T00:00:00.000Z",
      "sortOrder": 0,
      "published": true,
      "createdAt": "2026-05-13T10:42:00.123Z",
      "updatedAt": "2026-05-13T10:42:00.123Z"
    }
  },
  "error": null,
  "meta": { "requestId": "req_01HX...", "timestamp": "2026-05-13T10:42:00Z" }
}

Ids are always pfi_ + ULID.

Errors specific to this endpoint

Status error.code When
400 VALIDATION_ERROR title empty or over 140; externalUrl not https://; more than 8 images; an image that is a blob:/data: URL.
400 VALIDATION_ERROR productId does not name one of your products — the id is well-formed but belongs to another workspace, or does not exist.
401 UNAUTHORIZED Key missing, malformed, or revoked.

List items

GET /v1/portfolio

Returns every item for the workspace in render order (sortOrder ascending, then createdAt ascending), published and unpublished alike. There is no pagination and no filter on this endpoint — the credibility strip is small by design.

Response200 OK

{ "data": { "items": [ /* … */ ] }, "error": null, "meta": { /* … */ } }

Note the key is items (plural) here and item (singular) on the create/retrieve/update endpoints.

Retrieve one item

GET /v1/portfolio/:id

404 RESOURCE_NOT_FOUND when the id does not exist or belongs to another workspace — the two are deliberately indistinguishable.

Update an item

PATCH /v1/portfolio/:id

Partial update: send only the fields you want to change. Every field from the create body is editable, including sortOrder, which is how you reorder the strip.

An empty body is rejected with 400 VALIDATION_ERROR (no fields to update). Omitting productId leaves the existing link alone; sending null clears it.

Response200 OK, the full updated item under data.item.

Delete an item

DELETE /v1/portfolio/:id

Permanent. Unlike a product (which is archived and recoverable), a portfolio item is destroyed. There is no undo.

Response200 OK

{ "data": { "deleted": true }, "error": null, "meta": { /* … */ } }

The object

Field Type Nullable Description
id string no pfi_ + ULID.
accountId string no Owning workspace.
title string no Project name, ≤140.
description string no ≤5000. "" when unset, never null.
images array of strings no ≤8 URLs. [] when unset.
productId string yes Related product. Set to null if that product is later deleted — the item itself survives.
externalUrl string yes https:// case-study link.
clientName string yes ≤140.
completedAt string (ISO 8601) yes Rendered as the year.
sortOrder integer no Ascending. Default 0.
published boolean no Default true.
createdAt / updatedAt string (ISO 8601) no Server-managed.

Product deletion

Deleting a product does not delete portfolio items attached to it. The relation is ON DELETE SET NULL: the item stays, productId becomes null, and the card renders without the product link.

Next

Currency
Rupiah is paid by transfer or QRIS; US dollars settle through PayPal.