BuildNexion

Importar facturas a Quipu

Mismo patrón que la integración con Holded: escuchamos el evento invoice.created y empujamos la factura a Quipu vía su API REST.

Qué necesitas

  • API key de BuildNexion con scope read:invoices.
  • App key + secret de Quipu API.
  • Endpoint público HTTPS.

Paso 1 — Registrar el webhook

bash
curl -X POST https://api.buildnexion.com/v1/webhooks \
  -H "Authorization: Bearer bn_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url":    "https://mi-app.com/buildnexion/quipu-sync",
    "events": ["invoice.created", "invoice.updated"]
  }'

Paso 2 — Token OAuth de Quipu

Quipu usa OAuth2 con client_credentials. Pide un token y cacharrale en memoria durante 1 hora:

python
import os, time, requests

_token_cache = {"value": None, "expires_at": 0}

def quipu_token() -> str:
    if _token_cache["value"] and _token_cache["expires_at"] > time.time():
        return _token_cache["value"]

    res = requests.post(
        "https://getquipu.com/oauth/token",
        auth=(os.environ["QUIPU_APP_ID"], os.environ["QUIPU_APP_SECRET"]),
        data={"grant_type": "client_credentials"},
    )
    body = res.json()
    _token_cache["value"]      = body["access_token"]
    _token_cache["expires_at"] = time.time() + body["expires_in"] - 30
    return _token_cache["value"]

Paso 3 — Endpoint del webhook

python
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
import os, json, requests
from verify import verify_buildnexion_signature

app = FastAPI()

@app.post("/buildnexion/quipu-sync")
async def webhook(request: Request, bg: BackgroundTasks):
    raw = (await request.body()).decode("utf-8")
    sig = request.headers.get("buildnexion-signature", "")

    if not verify_buildnexion_signature(
        payload=raw,
        signature_header=sig,
        secret=os.environ["BUILDNEXION_WEBHOOK_SECRET"],
    ):
        raise HTTPException(401, "invalid signature")

    event = json.loads(raw)
    if event["type"] in ("invoice.created", "invoice.updated"):
        bg.add_task(sync_to_quipu, event["data"]["object"])
    return {"ok": True}

Paso 4 — Crear o actualizar el gasto en Quipu

python
def sync_to_quipu(invoice: dict):
    bn_key = os.environ["BUILDNEXION_API_KEY"]
    provider = requests.get(
        f"https://api.buildnexion.com/v1/providers/{invoice['provider_id']}",
        headers={"Authorization": f"Bearer {bn_key}"},
    ).json()

    headers = {
        "Authorization": f"Bearer {quipu_token()}",
        "Content-Type":  "application/vnd.api+json",
        "Accept":        "application/vnd.api+json",
    }

    # BuildNexion guarda céntimos; Quipu pide decimales como string
    subtotal = f"{invoice['subtotal_amount'] / 100:.2f}"
    total    = f"{invoice['total_amount']    / 100:.2f}"

    payload = {
        "data": {
            "type": "expenses",
            "attributes": {
                "issue_date":    invoice["issue_date"],
                "number":        invoice["invoice_number"],
                "subtotal":      subtotal,
                "total":         total,
                "supplier_name": provider["name"],
                "supplier_tin":  provider["nif"],
            }
        }
    }

    requests.post(
        "https://getquipu.com/api/expenses",
        headers=headers,
        json=payload,
    )
Como con Holded: añade idempotencia guardando event.id. Si reintentamos, no duplicarás el gasto en Quipu.

Errores típicos

  • Quipu devuelve 422 — número de factura duplicado para el mismo proveedor. Decide si actualizar o ignorar.
  • Token Quipu caduca — invalida el caché si recibes 401 y reintenta.
  • Importes mal sumados — recuerda que BuildNexion devuelve enteros en céntimos.