# Interact with Quable PIM API

By
The Quable Team

Overview
Installation
Initializing the client
REST API
-- Documents
-- Key-Value store
-- Users
-- Assets
GraphQL API
PIM Data Aggregator (PDA)
Practical examples
-- 1. Product listing with DataTable
-- 2. Key-Value form with mutations



# 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 code
  • documentType — { id: string } the document type
  • attributes — Localized attribute values
  • mainAssetThumbnailUrl — Thumbnail URL of the main asset
  • legacyId — 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,
});
Parameter Type Description
id string The unique key identifier
value string The stored value
isPublic boolean Whether the key is publicly accessible
isProtected boolean Whether the key is protected from deletion

# 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:

  • useQuery fetches data when the component mounts and caches the result.
  • DataTable from @quable/ui wraps MUI DataGrid Pro with Quable styling.
  • EmptyState from @quable/ui provides a consistent empty state UI.
  • GridColDef defines the columns with custom renderCell for 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:

  • useMutation wraps the Server Action for write operations.
  • react-hook-form handles validation and form state.
  • Error handling in the Server Action returns structured errors that the client can display.
  • toast from react-toastify provides user feedback.