Packs de productes
Ven diversos productes o serveis com un sol article. Llegeix el catàleg des del teu servidor, sincronitza'l amb webhooks, mostra preus amb i sense IVA i mapeja'l a schema.org.
Un pack agrupa dos o més productes o serveis sota un nom i un preu. El pack no té stock propi: la seva disponibilitat surt dels components, i en facturar-lo es converteix en un grup de línies fiscals normals. Els packs estan disponibles a tots els plans que inclouen productes, des d'Esencial. A Esencial un compte admet fins a 5 packs sense arxivar i la resta de plans no té límit.
Aquesta pàgina cobreix la lectura que necessita una web. Per vendre un pack, llegeix La teva botiga amb Factuarea. Per posar-lo en oferta, llegeix Ofertes i preu anterior.
Ús de servidor a servidor
L'API de packs està pensada per cridar-se des del servidor de la teva web (el backend que pinta les pàgines), mai des del navegador. No existeixen claus publicables: una crida des del navegador amb una API key la rebutja el preflight de CORS, i una clau dins del teu JavaScript exposaria tot l'abast del teu catàleg.
Crea una credencial per botiga amb els scopes mínims:
- Una API key de l'entorn
live, guardada només al teu servidor i enviada comAuthorization: Bearer fact_live_…. products:readipacks:readper llegir el catàleg.invoices:writenomés si aquest mateix servidor factura les comandes.- L'empresa va al camí (
/v1/companies/{company}/…), mai en una capçalera.
Una clau de només lectura que intenta canviar un pack rep
403 insufficient_scope i el pack no canvia. Consulta
API keys i
Scopes i irreversibilitat.
Operacions
| Operació | Scope |
|---|---|
GET /v1/companies/{company}/packs i GET …/packs/{pack} | packs:read |
GET …/packs/{pack}/price-breakdown | packs:read |
POST /v1/companies/{company}/packs i PATCH …/packs/{pack} | packs:write |
POST …/packs/{pack}/archive i POST …/packs/{pack}/restore | packs:write |
POST …/packs/{pack}/images, PATCH …/images/{image} i DELETE …/images/{image} | packs:write |
DELETE …/packs/{pack} | packs:delete |
Els llistats usen paginació per cursor (limit, starting_after,
ending_before) i filtren per search, slug, sku, tag, channel,
status, availability[in] i updated[gte]. sort admet created, name i
price. Un pack porta almenys dos components, o un amb quantitat superior a 1.
Si no, la resposta és 422 pack_composition_too_small.
El preu d'un pack es defineix amb pricing{mode, value, basis}:
mode | Significat |
|---|---|
fixed | Un preu fix per a tot el pack |
percent_off | La suma dels components menys un percentatge |
amount_off | La suma dels components menys un import |
sum | La suma simple dels components |
basis indica si value és net (sense IVA) o gross (amb IVA). En PATCH,
components i pricing substitueixen el valor anterior de manera atòmica.
Dues vistes: all i storefront
Tota lectura admet view=all (per defecte) o view=storefront.
view=all | view=storefront | |
|---|---|---|
| Quins packs | Tots, també els arxivats | Només els actius, amb canal web, en vigència i vendibles |
| Qualsevol altre pack | Es retorna amb el seu estat | El detall respon 404 pack_not_found i la cerca per slug surt buida |
| Camps extra | status, channels, pricing, sellable, availability.limited_by, attention | Cap |
| Mai es retorna | Costos ni ofertes de proveïdor | Costos, ofertes de proveïdor, limited_by, attention ni metadata |
Qualsevol altre valor de view respon 400 parameter_invalid_enum amb
param: "view". Fes servir storefront per pintar pàgines i all per
sincronitzar.
Disponibilitat i quantitats exactes
availability.status val in_stock, low, out_of_stock o untracked.
untracked vol dir que cap component controla stock i la teva web ha de tractar
el pack com a disponible. L'stock mai bloqueja una venda: un pack
out_of_stock es pot facturar igualment.
L'aparador mai dona quantitats exactes tret que l'empresa ho triï. Ho decideix
l'ajust d'empresa web_stock_display:
web_stock_display | Què retorna l'aparador |
|---|---|
status | Només l'estat |
low_only (per defecte) | L'estat, i quantity només quan l'estat és low |
exact | L'estat i quantity sempre |
L'ajust és el mateix per a productes, variants i presentacions. El component que limita l'stock no s'exposa mai a la web.
Preu amb i sense IVA
price és el preu del pack amb la tarifa base, al règim de l'empresa i per a
avui. Porta tots els imports que necessites per a una etiqueta de preu:
{
"id": "019f1c2e-7b40-7a11-9c35-2d4e6f8a0b12",
"object": "pack",
"name": "Pack Huerto urbano",
"slug": "pack-huerto-urbano",
"sku": "PACK-HUERTO",
"currency": "EUR",
"price": { "net": "50.53", "tax": "9.37", "gross": "59.90" },
"tax_breakdown": [
{ "rate": 21, "base": "39.27", "tax": "8.24" },
{ "rate": 10, "base": "11.26", "tax": "1.13" }
],
"separately": { "gross": "72.91" },
"savings": { "gross": "13.01", "percent": 17.8 },
"availability": { "status": "in_stock" },
"promotion": null
}L'exemple està retallat als camps de preu. Els imports són text decimal amb dos
decimals, i els percentatges i tipus d'IVA són números. separately és el que
costen els components comprats un a un. Quan no hi ha estalvi (un preu fixed
per sobre de la suma, o components en oferta), savings val null i la teva
web no ha de mostrar estalvi.
Canàries i operacions exemptes: fes servir net
price.gross és el preu amb l'IVA de l'empresa. Només és l'etiqueta
correcta per a un client al qual s'aplica aquest IVA. En altres règims el pack
conserva els seus imports nets i l'impost del document s'aplica a sobre. A
Canàries, el Pack Huerto urbano costa 50,53 € més IGIC, mai «59,90 € amb IGIC».
El mateix val per a vendes intracomunitàries, exportacions i inversió del
subjecte passiu, on l'impost és 0.
Si la teva botiga ven a aquests clients, construeix el preu a partir de
price.net i la teva pròpia regla d'impostos, i mostra'l com a «més impostos»
quan correspongui. Consulta IVA, IGIC i IPSI.
Preu en un altre context
GET …/packs/{pack}/price-breakdown admet price_list_id, customer_id i
date, i retorna l'import «per separat», el preu, l'estalvi, el
tax_breakdown i la part de cada component amb aquesta tarifa. Serveix per a
webs B2B amb tarifes de client.
Imatges
Un pack té fins a 5 imatges. Cadascuna porta id, url, thumb_url (480 px),
alt (fins a 300 caràcters), width, height, position i content_type.
cover es resol al servidor: la primera imatge del pack, si no la primera imatge
del primer component que en tingui, i si tampoc null, amb source: "own" o
"component". Cada component exposa image{url, alt} perquè componguis un
mosaic pel teu compte.
Les URL de les imatges són públiques i permanents: absolutes, en WebP, sense
dades EXIF, descarregables per HTTPS sense credencials, sense signatura i sense
caducitat. El seu nom és un UUID impossible d'endevinar que no es reutilitza, així
que pots enllaçar-les directament o copiar-les al teu CDN i guardar-les per
sempre. Substituir una imatge crea una URL nova i esborrar-la fa que la seva URL
respongui 404. Baixar-les no consumeix quota de l'API, i es serveixen amb
Cache-Control: public, max-age=31536000, immutable.
Per pujar-ne una, envia multipart/form-data amb el camp photo (màxim 3 MB,
els mateixos formats que la galeria de producte) a POST …/packs/{pack}/images.
La capçalera Idempotency-Key és obligatòria: sense ella la resposta és
422 idempotency_key_required. Una sisena imatge respon
422 pack_image_limit_reached.
Mantén la teva còpia sincronitzada
No llegeixis el llistat sencer en bucle. Guarda una còpia del catàleg al teu servidor i actualitza-la amb dos mecanismes:
- Webhooks. Subscriu-te a
pack.created,pack.updated,pack.archived,pack.restoredipack.deleted(consulta Webhooks). El payload dels quatre primers és el pack enview=all.pack.deletednomés porta{id, object: "pack", deleted: true}, perquè el pack ja no es pot llegir. updated[gte]com a xarxa de seguretat després d'una caiguda:GET …/packs?view=all&updated[gte]=2026-10-25T00:00:00Z.
Sincronitza amb view=all, no amb storefront: l'aparador només llista el que
avui és a la venda, així que no et diu què acaba de sortir-ne. Un pack que surt
de l'aparador apareix amb sellable.web: false i el seu motiu, i la teva web el
retira. Un pack esborrat només es coneix per pack.deleted o pel 404 de la seva
lectura.
updated_at canvia davant de qualsevol canvi que alteri la resposta: dades,
composició, preu derivat, imatges, estat de disponibilitat, un component que deixa
de ser vendible, i l'entrada o sortida de la vigència. Un programador de mitjanit,
a la zona horària de l'empresa, cobreix els canvis per data: un pack que acaba la
temporada el 30 de juny emet pack.updated l'1 de juliol sense que ningú l'editi.
La quantitat disponible és l'excepció. Una venda que baixa la quantitat sense
canviar l'estat no mou updated_at ni emet pack.updated. Si mostres quantitats
exactes, llegeix el detall del pack en pintar-lo.
Si reanomenes un slug, l'anterior passa a previous_slugs (fins a 10) i una
cerca per slug també el troba, perquè la teva web respongui un 301 a l'adreça
nova. Els slugs són únics entre productes i packs, així que tots dos poden viure
sota la mateixa ruta.
Lectures condicionals i cost d'un 304
Les lectures d'un pack, del llistat i del desglossament responen amb un ETag
fort (sha1 del cos servit), Cache-Control: private, no-cache i
Vary: Accept-Language, Authorization. Envia'l de tornada a If-None-Match i un
recurs sense canvis respon 304 sense cos. view=all i view=storefront tenen
ETag diferents.
Un 304 consumeix quota de l'API com qualsevol altra petició, així que
estalvia ample de banda, no peticions. Per estalviar peticions, recolza't en els
webhooks i en updated[gte], i serveix les teves pàgines des de la teva pròpia
memòria cau. Consulta Límits de peticions.
Contingut i SEO
Un pack té summary (text pla, fins a 200), description (text pla amb salts de
línia, fins a 5.000, mai HTML) i seo{title, description} (fins a 70 i 160). Els
camps SEO buits es resolen amb el nom i el resum. L'API no retorna frases
muntades al servidor com «Inclou…»: retorna components[].name, quantity i
unit{code, label} perquè composis el text en el teu idioma. Cada component porta
a més product_id i slug (null si aquell producte no és a la web), per
enllaçar-ne la fitxa des del pack.
schema.org
Mapeja un pack a un Product amb una Offer, i el llistat a un ItemList:
| schema.org | Camp del pack |
|---|---|
Offer.price | price.gross |
Offer.priceCurrency | currency |
Product.sku | sku |
Product.image | cover.url |
Offer.priceValidUntil | available_until (o promotion.ends_at durant una oferta) |
Offer.availability | InStock, LimitedAvailability o OutOfStock segons availability.status |
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Pack Huerto urbano",
"sku": "PACK-HUERTO",
"image": "https://app.factuarea.com/uploads/packs/01931b3e-1111-7a2e-9a8b-3c5d6e7f8a01/019f1c2e-7b40-7a11-9c35-2d4e6f8a0b12/019f1c31-aa10-7c02-8d4f-0e1b2c3d4e5f.webp",
"offers": {
"@type": "Offer",
"price": "59.90",
"priceCurrency": "EUR",
"availability": "https://schema.org/InStock"
}
}untracked es mapeja a InStock. Fes servir el preu amb IVA només per a clients
als quals s'aplica l'IVA de l'empresa, com s'explica a dalt.
Errors
| Status | code | Quan |
|---|---|---|
| 404 | pack_not_found | El pack no existeix, és d'una altra empresa o és fora de l'aparador |
| 409 | sku_already_exists | L'SKU xoca amb un producte, una variant o un altre pack |
| 409 | slug_already_exists | El slug el fa servir o el va fer servir un altre producte o pack |
| 422 | pack_in_use | Esborres un pack que és en documents |
| 422 | pack_composition_too_small | El pack té un únic component de quantitat 1 |
| 422 | invalid_pack_component i duplicate_pack_component | Un component no s'admet o està repetit |
| 422 | invalid_pack_pricing, pack_price_unreachable i pack_price_allocation_impossible | El preu no es pot fixar o repartir |
| 422 | invalid_pack_status_transition | Arxivar o restaurar no és possible des de l'estat actual |
| 422 | pack_image_limit_reached | Una sisena imatge |
| 402 | plan_limit_exceeded | Crees o restaures un pack i el compte Esencial ja té 5 packs sense arxivar (details.resource val packs) |
| 403 | feature_not_available_in_plan | L'empresa té desactivat el mòdul de productes |
Els errors de les línies de document (pack_not_sellable,
pack_snapshot_invalid, pack_lines_must_be_expanded, pack_line_locked i
pack_print_mode_not_allowed) s'expliquen a
La teva botiga amb Factuarea. El servidor MCP replica cada
operació amb els mateixos errors: list_packs, get_pack, create_pack,
update_pack, archive_pack, restore_pack, delete_pack,
get_pack_price_breakdown, upload_pack_image, update_pack_image i
delete_pack_image.