#
Interact with Quable PIM API
--
--
--
--
--
--
This is a guide to help you understand how your Quable App can interact with the Quable PIM using the @quable/quable-pim-js SDK (v3.0.0).
#
Overview
The @quable/quable-pim-js library provides a high-level abstraction over the Quable PIM API. It supports:
- REST API — CRUD operations on documents, variants, links, assets, classifications, users, and key-values.
- GraphQL API — Flexible queries with variables.
- PIM Data Aggregator (PDA) — Advanced data extraction and aggregation for bulk operations.
#
Installation
The SDK is already included in the app template. If you need to install it manually:
npm install @quable/quable-pim-js
# or
yarn add @quable/quable-pim-js
Requirements:
- Node.js >= 18.0.0
- A valid API token from your Quable PIM instance (Administration > API Token)
#
Initializing the client
Create a QuablePimClient instance with your instance name and API token:
import { QuablePimClient } from "@quable/quable-pim-js";
const client = new QuablePimClient({
instanceName: "my-instance",
apiToken: "my-api-token",
});
In the template, the client is typically created inside Server Actions using credentials from the database:
"use server";
import { QuablePimClient } from "@quable/quable-pim-js";
import prisma from "../prisma";
import { getCurrentSession } from "../session";
async function getQuablePimClient() {
const session = await getCurrentSession();
if (!session) throw new Error("Session not found");
const quableInstance = await prisma.quableInstance.findUniqueOrThrow({
where: { id: session.quableInstanceId },
});
return new QuablePimClient({
apiToken: quableInstance.token,
instanceName: quableInstance.name,
});
}
#
REST API
The REST API is accessible via client.API.REST. Each resource has its own set of methods.
#
Documents
// Get all documents (with optional filters)
const documents = await client.API.REST.Document.getAll({});
// Get all documents with filters
const filtered = await client.API.REST.Document.getAll({
limit: 10,
id: "my-document-id",
active: true,
});
// Create a document
const newDoc = await client.API.REST.Document.create({
id: "new-document-id",
documentTypeId: "product",
classificationId: "category-1",
attributes: {
product_name: { en_US: "My Product", fr_FR: "Mon Produit" },
},
});
// Update a document
const updatedDoc = await client.API.REST.Document.update("document-id", {
attributes: {
product_name: { en_US: "Updated Name" },
},
});
// Delete a document
await client.API.REST.Document.delete("document-id");
Each document returned by getAll() includes:
id— The document codedocumentType—{ id: string }the document typeattributes— Localized attribute valuesmainAssetThumbnailUrl— Thumbnail URL of the main assetlegacyId— Legacy numeric ID
#
Key-Value store
The Key-Value store allows your app to persist configuration data in the PIM:
import { KeyValueModel } from "@quable/quable-pim-js";
// Create a key-value pair
const kv: KeyValueModel = await client.API.REST.KeyValue.create({
id: "my-config-key",
value: "my-config-value",
isPublic: true,
isProtected: false,
});
#
Users
// Get users (useful for token validation during installation)
const users = await client.API.REST.User.getAll({
limit: 1,
type: "api",
});
This is used in the install route to validate the API token:
// src/app/install/route.ts
const pimClient = new QuablePimClient({
apiToken: quableAuthToken,
instanceName: quableInstanceName,
});
// This throws if the token is invalid
await pimClient.API.REST.User.getAll({ limit: 1, type: "api" });
#
Assets
// Get an asset by code
const asset = await client.API.REST.Asset.get("asset-code");
// Update an asset
await client.API.REST.Asset.update("asset-code", {
attributes: { alt_text: { en_US: "Product photo" } },
});
#
GraphQL API
For more flexible queries, use the GraphQL API:
// Simple query
const result = await client.API.GraphQL.query(`
{
documents(first: 10) {
edges {
node {
id
code
attributes
}
}
}
}
`);
// Query with variables
const result = await client.API.GraphQL.query(
`query GetDocument($code: String!) {
document(code: $code) {
id
code
attributes
}
}`,
{ code: "my-document-code" }
);
#
PIM Data Aggregator (PDA)
The PDA module is designed for extracting and aggregating data from documents with complex hierarchies.
#
Single query
// Get aggregated data for specific document IDs
const data = await client.API.PDA.Query.getData({
documentIds: ["doc-1", "doc-2", "doc-3"],
});
#
Bulk operations
For large datasets, use the async operation pattern:
// 1. Create a bulk aggregation operation
const operation = await client.API.PDA.Operation.create({
documentTypeId: "product",
});
// 2. Check operation status
const status = await client.API.PDA.Operation.get(operation.id);
// 3. Download results when complete
const results = await client.API.PDA.Operation.downloadAndUnzip(operation.id);
#
Practical examples
#
1. Product listing with DataTable
This example shows how to fetch documents from the PIM and display them in a data table.
#
Server Action (src/lib/actions/products.ts)
"use server";
import { QuablePimClient } from "@quable/quable-pim-js";
import prisma from "../prisma";
import { getCurrentSession } from "../session";
export async function getProducts(): Promise<
{ image: string | null; name: string; code: string; productType: string }[]
> {
try {
const session = await getCurrentSession();
if (!session) throw new Error("Session not found");
const quableInstance = await prisma.quableInstance.findUniqueOrThrow({
where: { id: session.quableInstanceId },
});
const quablePimClient = new QuablePimClient({
apiToken: quableInstance.token,
instanceName: quableInstance.name,
});
const products = await quablePimClient.API.REST.Document.getAll({});
return products.map((product) => {
const productName = product.attributes[`${product.documentType.id}_name`];
return {
image: product.mainAssetThumbnailUrl,
name: productName[session.dataLocale]
? productName[session.dataLocale]
: product.id,
code: product.id,
productType: product.documentType.id,
directUrl: `https://${quableInstance.name}.quable.com/#classification/product-new/${product.legacyId}/edit`,
};
});
} catch (error) {
console.error(error);
return [];
}
}
#
Client Component (src/app/[session]/product/page.tsx)
"use client";
import { useTranslations } from "next-intl";
import { DataTable, EmptyState } from "@quable/ui";
import { useQuery } from "@tanstack/react-query";
import { GridColDef } from "@mui/x-data-grid-pro";
import { Typography } from "@mui/material";
import { OpenInNew } from "@mui/icons-material";
import { getProducts } from "@/lib/actions/products";
import Image from "next/image";
export default function ProductsPage() {
const t = useTranslations("products_page");
const { data: products, isLoading } = useQuery({
queryKey: ["products"],
queryFn: () => getProducts(),
});
const columns: GridColDef[] = [
{
field: "image",
headerName: "",
sortable: false,
renderCell: (params) => {
if (!params.value) return <></>;
return <Image src={params.value} alt={params.row.name} width={100} height={100} />;
},
},
{
field: "code",
headerName: t("columns.product_code"),
sortable: false,
flex: 1,
},
{
field: "name",
headerName: t("columns.product_name"),
sortable: false,
flex: 1,
},
{
field: "productType",
headerName: t("columns.product_type"),
sortable: false,
flex: 1,
},
{
field: "action",
headerName: "",
sortable: false,
align: "right",
renderCell: (params) => (
<OpenInNew
sx={{ cursor: "pointer", color: "text.secondary" }}
fontSize="small"
onClick={() => window.open(params.row.directUrl, "_blank", "noopener,noreferrer")}
/>
),
},
];
return (
<div className="p-4">
<Typography sx={{ mb: 2 }} fontWeight={600} variant="body-lg">
{t("title")}
</Typography>
<DataTable
loading={isLoading}
columns={columns}
rowCount={products?.length || 0}
rows={products || []}
getRowId={(row) => row.code}
rootProps={{ style: { height: "90vh" } }}
slots={{
noRowsOverlay: () => (
<EmptyState title={t("no_products")} description={t("no_products_description")} />
),
}}
/>
</div>
);
}
Key patterns:
useQueryfetches data when the component mounts and caches the result.DataTablefrom@quable/uiwraps MUI DataGrid Pro with Quable styling.EmptyStatefrom@quable/uiprovides a consistent empty state UI.GridColDefdefines the columns with customrenderCellfor images and actions.
#
2. Key-Value form with mutations
This example shows how to write data to the PIM using a form.
#
Server Action (src/lib/actions/key-value.ts)
"use server";
import { getCurrentSession } from "../session";
import prisma from "../prisma";
import { KeyValueModel, QuablePimClient } from "@quable/quable-pim-js";
import { getTranslations } from "next-intl/server";
export async function addKeyValue(
key: string,
value: string
): Promise<{ keyValue?: KeyValueModel | null; error?: string | null }> {
const t = await getTranslations("common");
try {
const session = await getCurrentSession();
if (!session) throw new Error("Session not found");
const quableInstance = await prisma.quableInstance.findUniqueOrThrow({
where: { id: session.quableInstanceId },
});
const quablePimClient = new QuablePimClient({
apiToken: quableInstance.token,
instanceName: quableInstance.name,
});
const keyValue = await quablePimClient.API.REST.KeyValue.create({
isProtected: false,
isPublic: true,
id: key,
value: value,
});
return { keyValue, error: null };
} catch (error) {
if (typeof error === "object") {
const err = error as { response?: { code?: number; message?: string } };
if (err.response?.code === 400) {
return { keyValue: null, error: err.response.message };
}
}
return { keyValue: null, error: t("toasts.add_key_value_error") };
}
}
#
Client Component
"use client";
import { addKeyValue } from "@/lib/actions/key-value";
import { TextField, Button } from "@quable/ui";
import { useMutation } from "@tanstack/react-query";
import { useForm } from "react-hook-form";
import { toast } from "react-toastify";
export default function KeyValueForm() {
const { register, formState, handleSubmit, reset } = useForm<{
key: string;
value: string;
}>({ defaultValues: { key: "", value: "" }, mode: "onChange" });
const { isPending, mutateAsync } = useMutation({
mutationFn: (data: { key: string; value: string }) =>
addKeyValue(data.key, data.value),
onSuccess: (data) => {
if (data.keyValue) {
toast.success("Key-value added successfully");
reset();
} else {
toast.error(data.error);
}
},
});
return (
<form noValidate onSubmit={handleSubmit((data) => mutateAsync(data))}>
<TextField
disabled={isPending}
label="Key"
fullWidth
error={!!formState.errors.key}
{...register("key", { required: "Key is required" })}
/>
<TextField
disabled={isPending}
label="Value"
fullWidth
error={!!formState.errors.value}
{...register("value", { required: "Value is required" })}
/>
<Button isLoading={isPending} variant="contained" type="submit">
Save
</Button>
</form>
);
}
Key patterns:
useMutationwraps the Server Action for write operations.react-hook-formhandles validation and form state.- Error handling in the Server Action returns structured errors that the client can display.
toastfrom react-toastify provides user feedback.
You now know how to interact with the Quable PIM API using the @quable/quable-pim-js SDK. Next, we'll explore how to subscribe to and handle webhook events.