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": falseif 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:// only — http:// 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.
Response — 201 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.
Response — 200 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.
Response — 200 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.
Response — 200 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
- Testimonials — customer quotes, same conventions.
- Certifications — credentials and badges.
- Products — what
productIdpoints at.