La teva botiga amb Factuarea
Mostra productes i packs a la teva botiga web, mantén preus i disponibilitat sincronitzats i factura cada comanda pagada amb el preu que va pagar el client.
Aquesta guia mostra com una web com Vivero Los Olivos fa servir Factuarea com el seu catàleg i el seu motor de facturació. El teu servidor llegeix productes i packs, els mostra a les teves pàgines i, quan una comanda es paga, crea la factura.
Tot s'executa des del teu servidor, mai des del navegador. Llegeix abans Packs per a la credencial, les vistes i les regles de memòria cau, que són les mateixes per als productes.
Un aparador per a productes i packs
Productes i packs són dos recursos que comparteixen un contracte d'aparador. No existeix un llistat mixt: el teu servidor sincronitza els dos recursos i ordena, pagina i cerca sobre la seva pròpia còpia. Així cada recurs conserva el seu cursor, els seus filtres i la seva memòria cau.
curl "https://api.factuarea.com/v1/companies/$COMPANY_ID/products?view=storefront&tag=huerto" \
-H "Authorization: Bearer fact_live_…"
curl "https://api.factuarea.com/v1/companies/$COMPANY_ID/packs?view=storefront&tag=huerto" \
-H "Authorization: Bearer fact_live_…"Amb view=storefront els dos recursos publiquen els mateixos camps:
| Camp | Producte | Pack |
|---|---|---|
| Identitat i contingut | id, object, name, slug, previous_slugs, summary, description, seo, tags, currency, updated_at | Els mateixos |
| Preu amb i sense IVA | sale_price{net, tax, gross} | price{net, tax, gross} |
| Disponibilitat | availability{status, quantity?} | La mateixa |
| Imatges | gallery[] amb alt, width, height | cover i gallery[] amb alt |
| Oferta | promotion | promotion |
El preu del producte es diu sale_price perquè price ja era un text publicat i
no pot canviar de tipus. Fora de l'aparador els dos recursos responen igual:
404 al detall i llistat buit en cercar per slug. Un component de pack porta
product_id i slug, perquè la teva pàgina enllaci el producte (slug és
null si aquell producte no és a la web).
Pas a pas
- Crea la credencial. Una API key
liveguardada al teu servidor ambproducts:read,packs:readi, si factures,invoices:write. - Publica els productes. Un producte no és a la web fins que el seu
channels.webéstrue. Valfalseper defecte, també en els productes existents, perquè res no es publiqui sense una decisió. Publica'ls un a un ambPATCH …/products/{product}ichannels: {web: true}, o en bloc ambPOST …/products/bulk-channels(vegeu més avall). En publicar es genera el slug si falta. - Fes la primera còpia. Recorre
GET …/products?view=storefrontiGET …/packs?view=storefrontamb el cursor i guarda els resultats. - Mantén-la al dia. Subscriu-te als webhooks
product.*ipack.*, inclososproduct.deletedipack.deleted, i fes servirview=all&updated[gte]=…per recuperar-te després d'una caiguda. Un producte que surt de la web hi apareix ambis_active: falseochannels.web: false. - Pinta les teves pàgines des de la teva còpia, i respon 301 quan una petició
faci servir un slug que ara és a
previous_slugs.
Publicar en bloc
POST /v1/companies/{company}/products/bulk-channels rep
{"ids": [...], "channels": {"web": true}} amb fins a 50 ids per lot. La
capçalera Idempotency-Key és obligatòria i cada fila s'encerta o falla pel seu
compte: si un id és d'una altra empresa, aquella fila falla com a inexistent i la
resta es publica, i la resposta ho indica per fila. Fes servir "web": false per
retirar-los.
Preus amb i sense IVA
Mostra sale_price.gross (o price.gross) als clients que paguen l'IVA de
l'empresa. L'olivera costa 39,00 € sense IVA i 42,90 € amb IVA:
{ "price": "39.00", "sale_price": { "net": "39.00", "tax": "3.90", "gross": "42.90" } }price conserva el seu significat de sempre. Per a clients de Canàries, exempts i
d'exportació, construeix el preu a partir de net: l'impost del document
s'aplica a sobre i «59,90 € amb IGIC» no existeix. Els detalls són a
Packs. Un producte sense impost assignat retorna tax: "0.00" i
gross igual a net. Per a una combinació lliure d'opcions,
POST …/products/{product}/resolve-selection retorna el sale_price d'aquella
combinació.
Disponibilitat
availability.status val in_stock, low, out_of_stock o untracked (no es
controla l'stock, tracta'l com a disponible). Una presentació compta en les seves
pròpies unitats comercials: una caixa de 6 amb 20 unitats en stock són 3 caixes.
L'aparador mai dona quantitats exactes tret que l'empresa posi
web_stock_display=exact. El valor per defecte, low_only, dona la quantitat
només quan l'estat és low. L'stock mai bloqueja una venda, així que no amaguis
pel teu compte el botó de compra en out_of_stock tret que sigui la teva política.
Imatges
Les imatges de producte i de pack segueixen la mateixa regla: URL absolutes
públiques i permanents, sense credencial, signatura ni caducitat, amb un nom UUID
que no es reutilitza. Enllaça-les directament o copia-les al teu CDN. Tota imatge
porta un alt (la galeria de producte fa servir el nom del producte) i, en
productes, width i height per reservar l'espai abans de carregar-la.
Memòria cau
Mantén la teva pròpia memòria cau i recolza't en els webhooks per refrescar-la. Si
tot i això revalides, envia If-None-Match amb l'ETag que vas guardar: un recurs
sense canvis respon 304 sense cos. Recorda que un 304 consumeix quota de
l'API. Productes i packs fan servir les mateixes capçaleres.
Webs B2B amb tarifes de client
Si la teva web ven a empreses que tenen la seva pròpia tarifa, demana el preu de cada client en lloc de mostrar el públic.
1. Demana amb client_id. Envia el contacte del client que ha iniciat sessió
(el mateix client_id que porta una factura) a POST …/price-lists/resolve, o a
…/resolve-many per a fins a 100 productes. Afegeix channel: "web" perquè
s'apliquin les ofertes de la web:
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/price-lists/resolve \
-H "Authorization: Bearer fact_live_…" \
-H "Content-Type: application/json" \
-d '{
"product_id": "019f1b77-3e90-7c24-8a1d-5f0b2e6c9d48",
"client_id": "019f2a0c-1d52-7b8e-a4f0-3c6d9e2b5a17",
"channel": "web"
}'La tarifa es tria com en un document. Un price_list_id que enviïs mana (i null
significa sense tarifa). Si no l'envies, regeix la tarifa predeterminada del
client, i s'ignora si és inactiva. La resposta inclou unit_price, el source i
el nom de la tarifa, regular_unit_price i, quan s'aplica una oferta,
promotion.
2. Mostra gross i cobra l'import exacte. El gross de promotion.price
usa l'IVA del producte i el règim de l'empresa. No aplica el recàrrec
d'equivalència, l'exempció ni l'IGIC del client. Per mostrar o cobrar l'import
exacte, envia les línies a POST …/taxes/calculate-totals: és el mateix càlcul
que fa la factura.
3. Mantén la memòria cau al dia. Subscriu-te als webhooks price_list.*
(created, updated, deleted, item_updated i item_deleted) i refresca
només el que ha canviat. Després d'una caiguda, o en començar, posa't al dia
llistant amb updated[gte] igual a l'últim updated_at que vas desar, a
GET …/price-lists i a GET …/price-lists/{priceList}/items. L'updated_at d'una
tarifa també es mou quan canvia un dels seus preus.
Factura una comanda pagada
Quan el pagament es confirma, el teu servidor crea la factura amb
POST /v1/companies/{company}/invoices. L'id de la comanda va a external_id i
cada línia porta la data de la venda i el preu realment cobrat:
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/invoices \
-H "Authorization: Bearer fact_live_…" \
-H "Idempotency-Key: 019f2a10-5c3e-7d41-8b20-6a9e1c4d7f33" \
-H "Content-Type: application/json" \
-d '{
"client_id": "019f2a0c-1d52-7b8e-a4f0-3c6d9e2b5a17",
"issued_on": "2026-06-30",
"due_on": "2026-06-30",
"external_id": "WEB-1042",
"prices_include_tax": true,
"lines": [
{ "pack_id": "019f1c2e-7b40-7a11-9c35-2d4e6f8a0b12", "quantity": 2,
"sold_on": "2026-06-30", "channel": "web",
"unit_price": "59.90", "prices_include_tax": true },
{ "product_id": "019f1b77-3e90-7c24-8a1d-5f0b2e6c9d48", "quantity": 1,
"sold_on": "2026-06-30", "channel": "web",
"unit_price": "42.90", "prices_include_tax": true }
],
"payment": { "method": "credit_card", "reference": "WEB-1042-PAY" }
}'| Camp | Què fa |
|---|---|
external_id | L'id de la teva comanda. Fa la factura idempotent |
sold_on | El dia de la venda. Amb ell s'avaluen la vigència i les ofertes |
unit_price | El preu cobrat, per pack o per unitat. Guanya a qualsevol preu o oferta vigent |
prices_include_tax | Si aquest unit_price porta IVA. Funciona per línia i, si s'omet, la línia pren el del document |
channel | web perquè s'apliqui una oferta només web. Per defecte és documents |
payment | Marca la factura com a pagada i converteix la petició en un cobrament (vegeu Cobrament desatès) |
Regles que has de seguir
- Envia sempre el preu cobrat. Sense
unit_price, un pack es valora al preu vigent quan es crea la factura, no al que va veure el client. Si el pack va passar de 59,90 € a 64,90 € entre la pàgina i el pagament, enviarunit_price: "59.90"factura el que es va pagar. - La suma quadra al cèntim. Amb preu cobrat, el grup de línies es reparteix perquè sumi exactament el que s'ha pagat. Tres Pack Huerto urbano a 59,90 € sumen exactament 179,70 € i, si cal, una línia de component es parteix en dues (per exemple 17 i 1 unitats de plançons). El mateix val per a un producte amb preu amb IVA: una olivera cobrada a 36,47 € queda en una línia de 33,15 € més 3,32 € d'IVA.
- Una venda cobrada sempre es factura. Si el pack es va arxivar o va acabar la
vigència després del pagament, una línia amb
sold_oniunit_pricees factura igualment, amb la composició actual del pack. - Una venda al preu d'oferta compta com a venuda amb l'oferta. Si el
unit_pricecobrat coincideix amb el preu d'oferta d'aquell canal i aquella data, la línia conserva l'oferta. Consulta Ofertes. - L'stock es mou quan el mou el tipus de document, pel llibre d'stock normal. El pack no té stock propi.
Reintents
Dos mecanismes treballen junts. La capçalera Idempotency-Key cobreix el reintent
de transport de la mateixa petició. L'external_id identifica la comanda i no
caduca mai:
- Amb
payment,type: F2ooptions.register_verifactu, repetir la petició amb el mateixexternal_idrespon200amb la mateixa factura iIdempotent-Replayed: true, sense una segona factura. Un altre total o tipus respon409 unattended_replay_mismatch. - Sense ells, un reenviament actualitza l'esborrany existent en lloc de crear-ne un
altre, i si el document ja no és editable respon amb el seu error
422d'estat.
Reintenta els timeouts i els 5xx amb la mateixa clau i el mateix external_id.
No reintentis un 4xx sense corregir la petició.
Errors 422 previsibles
| Status | code | Causa |
|---|---|---|
| 404 | pack_not_found | El pack no existeix o és d'una altra empresa |
| 422 | pack_not_sellable | Arxivat, inactiu, fora de vigència a sold_on o amb un component inactiu. Només afecta línies sense unit_price, amb el motiu i param a la línia |
| 422 | quantity | La quantitat de packs no és entera, és menor que 1, o una línia resultant supera el sostre de la seva columna |
| 422 | pack_print_mode_not_allowed | El mode d'impressió no és vàlid per a aquell tipus de document |
| 422 | pack_lines_must_be_expanded | Vas enviar línies d'un pack construïdes per tu |
| 422 | simplified_invoice_not_allowed | Una factura simplificada per sobre del seu topall |
No pots construir les línies d'un pack pel teu compte: envia {pack_id, quantity}
i el servidor les expandeix. Per editar un esborrany sense perdre'n la fotografia,
reenvia {pack_group_id, quantity}. Editar el preu o la quantitat d'una línia de
component d'un grup respon 422 pack_line_locked.
Propers passos
- Packs: vistes, sincronització i schema.org.
- Ofertes i preu anterior: ofertes i les regles legals per anunciar una rebaixa.
- Webhooks i Idempotència.