Autenticación
Envía tu clave en la cabecera Authorization. Las claves se muestran una sola vez; guárdalas en el servidor, nunca en el navegador ni en una app.
curl https://delivery.piqueku.com/api/v1/pricing \
-H "Authorization: Bearer pkd_test_xxxxxxxx_xxxxxxxxxxxxxxxx"
Sandbox y producción
- pkd_test_: entorno de pruebas. Todo funciona igual (precios, estados, webhooks) pero nada llega a repartidores reales ni se factura. Avanza los estados con
POST /sandbox/deliveries/{id}/advance. Los webhooks llevan "simulated": true. - pkd_live_: producción. Disponible cuando tu empresa está verificada y activada (desde el portal, con al menos una dirección de recogida).
- Los datos de cada entorno están separados: una clave de pruebas nunca ve repartos reales y al revés.
Flujo de un reparto
Estados: CREATED → PENDING_ASSIGNMENT → ASSIGNED → COURIER_ACCEPTED → GOING_TO_PICKUP → ARRIVED_AT_PICKUP → PICKED_UP → IN_TRANSIT → ARRIVED_AT_DESTINATION → DELIVERED. Alternativas: DELIVERY_FAILED (→ RETURN_REQUESTED → RETURNED) y CANCELLED (solo antes de la recogida).
# 1. Precio
curl -X POST https://delivery.piqueku.com/api/v1/quotes \
-H "Authorization: Bearer $PKD_KEY" -H "Content-Type: application/json" \
-d '{"pickup":{"address":"Calle Mesones 5, Granada","lat":37.1752,"lng":-3.6001},
"dropoff":{"address":"Calle Recogidas 20, Granada","lat":37.171,"lng":-3.6045}}'
# 2. Crear el reparto (reintenta sin miedo con la misma Idempotency-Key)
curl -X POST https://delivery.piqueku.com/api/v1/deliveries \
-H "Authorization: Bearer $PKD_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-1042" \
-d '{"quote_id":"<id>","external_reference":"PEDIDO-1042",
"pickup":{"name":"Tienda Centro","phone":"+34600000001","address":"Calle Mesones 5, Granada","lat":37.1752,"lng":-3.6001},
"dropoff":{"name":"Pedro García","phone":"+34600000002","email":"cliente@example.com","address":"Calle Recogidas 20, Granada","lat":37.171,"lng":-3.6045}}'
# 3. Seguimiento: webhooks, GET /deliveries/{id}/tracking o el tracking_url para tu cliente
Idempotencia
En los POST, la cabecera Idempotency-Key (hasta 100 caracteres) hace que repetir la misma petición durante 24 h devuelva la misma respuesta (cabecera Idempotent-Replayed: true) en vez de crear otro reparto. Si reutilizas la clave con otro cuerpo recibes 409 idempotency_conflict.
Webhooks
Eventos: delivery.created, delivery.quoted, delivery.assigned, delivery.accepted, delivery.courier_arriving, delivery.picked_up, delivery.in_transit, delivery.delivered, delivery.failed, delivery.cancelled y delivery.status_changed (el resto de estados). Reintentos con espera creciente durante unas 20 horas (10 s, 1 min, 5 min, 30 min, 2 h, 6 h, 12 h); responde 2xx en menos de 10 s. Puedes reenviar cualquier evento desde el portal. Cada evento tiene un id estable para descartar duplicados.
Firma: cabecera PKD-Signature: t=<unix>,v1=<hex>, con v1 = HMAC-SHA256(secreto, "t.cuerpo"). Tras rotar el secreto, durante 24 h llegan dos v1 (nuevo y anterior): acepta si coincide cualquiera. Rechaza firmas con más de 5 minutos.
// Node.js (Express): verificar la firma con el cuerpo SIN parsear
import crypto from "node:crypto";
app.post("/webhooks/piqueku", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("PKD-Signature") ?? "";
const t = header.match(/t=(\d+)/)?.[1];
const sigs = [...header.matchAll(/v1=([a-f0-9]+)/g)].map((m) => m[1]);
const expected = crypto.createHmac("sha256", process.env.PKD_WEBHOOK_SECRET).update(`${t}.${req.body}`).digest("hex");
const fresh = t && Math.abs(Date.now() / 1000 - Number(t)) < 300;
const valid = sigs.some((s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
if (!fresh || !valid) return res.status(400).end();
const event = JSON.parse(req.body);
// event.type, event.data.delivery_id, event.data.status, event.data.tracking_url
res.status(200).end();
});
Errores y límites
Formato: {"error":{"code","message","details","request_id"}}. Cada respuesta lleva X-Request-Id. Límites: 300 peticiones/minuto por clave de producción y 120 por clave de pruebas, más el cupo mensual de tu plan.
| Código | HTTP | Significado |
|---|
| invalid_api_key | 401 | Falta la clave, no es válida o está revocada |
| insufficient_scope | 403 | La clave no tiene el permiso necesario |
| account_suspended | 403 | La empresa no está activa |
| forbidden | 403 | Producción sin activar o recurso de otra empresa |
| invalid_input | 400 | Campo con formato incorrecto (details.field) |
| invalid_json | 400 | El cuerpo no es JSON |
| not_covered | 400 | Dirección fuera de las zonas de servicio |
| too_far | 400 | Distancia mayor que la máxima del servicio |
| service_unavailable | 400 | Ese servicio no está activo en la ciudad |
| quote_expired | 400 | El presupuesto ha caducado |
| not_cancellable | 400 | Ya se ha recogido |
| invalid_transition | 400 | Cambio de estado no permitido |
| not_found | 404 | No existe o no es tuyo |
| conflict | 409 | Referencia externa repetida |
| idempotency_conflict | 409 | Misma Idempotency-Key con otro cuerpo |
| plan_limit_reached | 402 | Límite de repartos (402) o peticiones (429) del plan |
| rate_limited | 429 | Demasiadas peticiones por minuto |
| internal_error | 500 | Error nuestro: reintenta con la misma Idempotency-Key |
Referencia
POST/api/v1/quotespermiso quotes:write
Calcular precio
Precio de un reparto (antes de IVA, con el IVA aparte) y validez del presupuesto. Rechaza direcciones fuera de cobertura (not_covered) o demasiado lejanas (too_far).
service_type — immediate | express | scheduled (con scheduled_at ISO 8601, 30 min – 30 días)pickup / dropoff — address, lat, lng obligatorios (la API no geocodifica). O pickup_address_id de tus direcciones guardadasextra_stops — hasta 5 paradas intermedias con el mismo formatopackage — size (small|medium|large|xl), weight_kg, description, value_cents, fragile
Petición
{
"service_type": "immediate",
"pickup": {
"address": "Calle Mesones 5, Granada",
"lat": 37.1752,
"lng": -3.6001
},
"dropoff": {
"address": "Calle Recogidas 20, Granada",
"lat": 37.171,
"lng": -3.6045
},
"package": {
"weight_kg": 2
}
}
Respuesta
{
"id": "b4c…",
"environment": "sandbox",
"service_type": "immediate",
"city": "granada",
"zone": "Centro",
"distance_m": 791,
"estimated_duration_s": 142,
"price": {
"amount_cents": 390,
"tax_cents": 82,
"total_cents": 472,
"currency": "EUR"
},
"breakdown": {
"base_cents": 390,
"distance_cents": 0,
"extra_stops_cents": 0,
"heavy_cents": 0,
"minimum_applied": false,
"included_km": 2,
"billable_km": 0,
"vat_rate": 0.21
},
"expires_at": "2026-11-04T18:15:00Z"
}
GET/api/v1/coverage
Cobertura
Ciudades activas con sus zonas (GeoJSON), o comprobación de un punto con ?lat=&lng=.
lat, lng — opcional: comprueba si ese punto está cubierto
Respuesta
{
"covered": true,
"city": "granada",
"zone": "Centro",
"services": [
"immediate",
"express",
"scheduled"
]
}
GET/api/v1/availability
Disponibilidad ahora
Si se puede atender un reparto ahora en ese punto de recogida (cifras agregadas, sin datos de repartidores).
lat, lng — punto de recogida (obligatorio)
Respuesta
{
"available": true,
"city": "granada",
"services": [
"immediate",
"express",
"scheduled"
],
"couriers_available": 4
}
POST/api/v1/deliveriespermiso deliveries:write
Crear reparto
Crea el reparto y lo pasa al motor de asignación (los programados esperan a su ventana). Usa la cabecera Idempotency-Key para reintentar sin duplicar. Con claves pkd_test_ el reparto es de prueba y nunca llega a repartidores reales.
quote_id — opcional: presupuesto previo vigenteexternal_reference — tu número de pedido (único por empresa y entorno)require_pin — si es true, la respuesta incluye delivery_pin y el repartidor debe pedirlo al entregarsource — api | woocommerce | shopify | prestashop | magento | piqueku
Petición
{
"service_type": "immediate",
"external_reference": "PEDIDO-1042",
"pickup": {
"name": "Tienda Centro",
"phone": "+34600000001",
"address": "Calle Mesones 5, 18001 Granada",
"lat": 37.1752,
"lng": -3.6001,
"instructions": "Portal 2, 3ºB"
},
"dropoff": {
"name": "Pedro García",
"phone": "+34600000002",
"address": "Calle Recogidas 20, 18005 Granada",
"lat": 37.171,
"lng": -3.6045,
"instructions": "Portal 2, 3ºB",
"email": "cliente@example.com"
},
"package": {
"size": "small",
"weight_kg": 2,
"description": "Ropa"
},
"require_pin": false
}
Respuesta
{
"id": "8f3e2a1c-5b7d-4c1e-9a2f-1d2c3b4a5e6f",
"code": "PKD-7F3K2Q",
"environment": "sandbox",
"external_reference": "PEDIDO-1042",
"status": "PENDING_ASSIGNMENT",
"service_type": "immediate",
"scheduled_at": null,
"pickup": {
"name": "Tienda Centro",
"phone": "+34600000001",
"address": "Calle Mesones 5, 18001 Granada",
"lat": 37.1752,
"lng": -3.6001,
"instructions": "Portal 2, 3ºB"
},
"dropoff": {
"name": "Pedro García",
"phone": "+34600000002",
"address": "Calle Recogidas 20, 18005 Granada",
"lat": 37.171,
"lng": -3.6045,
"instructions": "Portal 2, 3ºB"
},
"extra_stops": [],
"package": {
"size": "small",
"weight_kg": 2,
"description": "Ropa",
"fragile": false
},
"distance_m": 791,
"estimated_duration_s": 142,
"price": {
"amount_cents": 390,
"tax_cents": 82,
"total_cents": 472,
"currency": "EUR"
},
"courier_assigned": false,
"tracking_url": "https://delivery.piqueku.com/t/…",
"cancel_reason": null,
"failure_reason": null,
"timestamps": {
"created_at": "2026-11-04T18:00:00Z",
"assigned_at": null,
"accepted_at": null,
"picked_up_at": null,
"delivered_at": null,
"failed_at": null,
"cancelled_at": null
}
}
GET/api/v1/deliveriespermiso deliveries:read
Listar repartos
Repartos del entorno de la clave, del más reciente al más antiguo.
status — filtra por estadoexternal_reference — tu referencialimit — 1–100 (20)starting_after — id del último de la página anterior
Respuesta
{
"data": [
{
"id": "8f3e2a1c-5b7d-4c1e-9a2f-1d2c3b4a5e6f",
"code": "PKD-7F3K2Q",
"environment": "sandbox",
"external_reference": "PEDIDO-1042",
"status": "PENDING_ASSIGNMENT",
"service_type": "immediate",
"scheduled_at": null,
"pickup": {
"name": "Tienda Centro",
"phone": "+34600000001",
"address": "Calle Mesones 5, 18001 Granada",
"lat": 37.1752,
"lng": -3.6001,
"instructions": "Portal 2, 3ºB"
},
"dropoff": {
"name": "Pedro García",
"phone": "+34600000002",
"address": "Calle Recogidas 20, 18005 Granada",
"lat": 37.171,
"lng": -3.6045,
"instructions": "Portal 2, 3ºB"
},
"extra_stops": [],
"package": {
"size": "small",
"weight_kg": 2,
"description": "Ropa",
"fragile": false
},
"distance_m": 791,
"estimated_duration_s": 142,
"price": {
"amount_cents": 390,
"tax_cents": 82,
"total_cents": 472,
"currency": "EUR"
},
"courier_assigned": false,
"tracking_url": "https://delivery.piqueku.com/t/…",
"cancel_reason": null,
"failure_reason": null,
"timestamps": {
"created_at": "2026-11-04T18:00:00Z",
"assigned_at": null,
"accepted_at": null,
"picked_up_at": null,
"delivered_at": null,
"failed_at": null,
"cancelled_at": null
}
}
],
"has_more": false
}
GET/api/v1/deliveries/{id}permiso deliveries:read
Ver un reparto
Por id o por código PKD-XXXXXX.
Respuesta
{
"id": "8f3e2a1c-5b7d-4c1e-9a2f-1d2c3b4a5e6f",
"code": "PKD-7F3K2Q",
"environment": "sandbox",
"external_reference": "PEDIDO-1042",
"status": "PENDING_ASSIGNMENT",
"service_type": "immediate",
"scheduled_at": null,
"pickup": {
"name": "Tienda Centro",
"phone": "+34600000001",
"address": "Calle Mesones 5, 18001 Granada",
"lat": 37.1752,
"lng": -3.6001,
"instructions": "Portal 2, 3ºB"
},
"dropoff": {
"name": "Pedro García",
"phone": "+34600000002",
"address": "Calle Recogidas 20, 18005 Granada",
"lat": 37.171,
"lng": -3.6045,
"instructions": "Portal 2, 3ºB"
},
"extra_stops": [],
"package": {
"size": "small",
"weight_kg": 2,
"description": "Ropa",
"fragile": false
},
"distance_m": 791,
"estimated_duration_s": 142,
"price": {
"amount_cents": 390,
"tax_cents": 82,
"total_cents": 472,
"currency": "EUR"
},
"courier_assigned": false,
"tracking_url": "https://delivery.piqueku.com/t/…",
"cancel_reason": null,
"failure_reason": null,
"timestamps": {
"created_at": "2026-11-04T18:00:00Z",
"assigned_at": null,
"accepted_at": null,
"picked_up_at": null,
"delivered_at": null,
"failed_at": null,
"cancelled_at": null
}
}
GET/api/v1/deliveries/{id}/trackingpermiso deliveries:read
Seguimiento
Estado, pasos, llegada estimada y posición del repartidor mientras va de camino (si está permitido en la plataforma).
Respuesta
{
"code": "PKD-7F3K2Q",
"status": "IN_TRANSIT",
"steps": [
{
"status": "CREATED",
"at": "…"
},
{
"status": "COURIER_ACCEPTED",
"at": "…"
},
{
"status": "PICKED_UP",
"at": "…"
},
{
"status": "IN_TRANSIT",
"at": "…"
},
{
"status": "DELIVERED",
"at": null
}
],
"eta_minutes": 6,
"courier": {
"first_name": "Lucía",
"vehicle": "Moto",
"position": {
"lat": 37.173,
"lng": -3.602,
"at": "…"
}
},
"tracking_url": "…"
}
POST/api/v1/deliveries/{id}/cancelpermiso deliveries:write
Cancelar
Solo antes de la recogida (error not_cancellable después).
Petición
{
"reason": "El cliente ha cancelado el pedido"
}
Respuesta
{
"id": "8f3e2a1c-5b7d-4c1e-9a2f-1d2c3b4a5e6f",
"code": "PKD-7F3K2Q",
"environment": "sandbox",
"external_reference": "PEDIDO-1042",
"status": "CANCELLED",
"service_type": "immediate",
"scheduled_at": null,
"pickup": {
"name": "Tienda Centro",
"phone": "+34600000001",
"address": "Calle Mesones 5, 18001 Granada",
"lat": 37.1752,
"lng": -3.6001,
"instructions": "Portal 2, 3ºB"
},
"dropoff": {
"name": "Pedro García",
"phone": "+34600000002",
"address": "Calle Recogidas 20, 18005 Granada",
"lat": 37.171,
"lng": -3.6045,
"instructions": "Portal 2, 3ºB"
},
"extra_stops": [],
"package": {
"size": "small",
"weight_kg": 2,
"description": "Ropa",
"fragile": false
},
"distance_m": 791,
"estimated_duration_s": 142,
"price": {
"amount_cents": 390,
"tax_cents": 82,
"total_cents": 472,
"currency": "EUR"
},
"courier_assigned": false,
"tracking_url": "https://delivery.piqueku.com/t/…",
"cancel_reason": "El cliente ha cancelado el pedido",
"failure_reason": null,
"timestamps": {
"created_at": "2026-11-04T18:00:00Z",
"assigned_at": null,
"accepted_at": null,
"picked_up_at": null,
"delivered_at": null,
"failed_at": null,
"cancelled_at": null
}
}
POST/api/v1/deliveries/{id}/assignpermiso deliveries:write
Pedir asignación
Producción: relanza el motor de asignación para un reparto pendiente (tú no eliges a la persona; el repartidor siempre acepta o rechaza). Sandbox: simula la asignación.
Respuesta
{
"id": "8f3e2a1c-5b7d-4c1e-9a2f-1d2c3b4a5e6f",
"code": "PKD-7F3K2Q",
"environment": "sandbox",
"external_reference": "PEDIDO-1042",
"status": "ASSIGNED",
"service_type": "immediate",
"scheduled_at": null,
"pickup": {
"name": "Tienda Centro",
"phone": "+34600000001",
"address": "Calle Mesones 5, 18001 Granada",
"lat": 37.1752,
"lng": -3.6001,
"instructions": "Portal 2, 3ºB"
},
"dropoff": {
"name": "Pedro García",
"phone": "+34600000002",
"address": "Calle Recogidas 20, 18005 Granada",
"lat": 37.171,
"lng": -3.6045,
"instructions": "Portal 2, 3ºB"
},
"extra_stops": [],
"package": {
"size": "small",
"weight_kg": 2,
"description": "Ropa",
"fragile": false
},
"distance_m": 791,
"estimated_duration_s": 142,
"price": {
"amount_cents": 390,
"tax_cents": 82,
"total_cents": 472,
"currency": "EUR"
},
"courier_assigned": true,
"tracking_url": "https://delivery.piqueku.com/t/…",
"cancel_reason": null,
"failure_reason": null,
"timestamps": {
"created_at": "2026-11-04T18:00:00Z",
"assigned_at": null,
"accepted_at": null,
"picked_up_at": null,
"delivered_at": null,
"failed_at": null,
"cancelled_at": null
}
}
POST/api/v1/sandbox/deliveries/{id}/advancepermiso deliveries:write
Sandbox: avanzar
Solo con claves pkd_test_: mueve el reparto de prueba al siguiente estado (next), lo hace fallar (fail) o lo cancela (cancel). Genera los mismos webhooks que en producción, marcados simulated: true.
Petición
{
"outcome": "next"
}
Respuesta
{
"id": "8f3e2a1c-5b7d-4c1e-9a2f-1d2c3b4a5e6f",
"code": "PKD-7F3K2Q",
"environment": "sandbox",
"external_reference": "PEDIDO-1042",
"status": "ASSIGNED",
"service_type": "immediate",
"scheduled_at": null,
"pickup": {
"name": "Tienda Centro",
"phone": "+34600000001",
"address": "Calle Mesones 5, 18001 Granada",
"lat": 37.1752,
"lng": -3.6001,
"instructions": "Portal 2, 3ºB"
},
"dropoff": {
"name": "Pedro García",
"phone": "+34600000002",
"address": "Calle Recogidas 20, 18005 Granada",
"lat": 37.171,
"lng": -3.6045,
"instructions": "Portal 2, 3ºB"
},
"extra_stops": [],
"package": {
"size": "small",
"weight_kg": 2,
"description": "Ropa",
"fragile": false
},
"distance_m": 791,
"estimated_duration_s": 142,
"price": {
"amount_cents": 390,
"tax_cents": 82,
"total_cents": 472,
"currency": "EUR"
},
"courier_assigned": false,
"tracking_url": "https://delivery.piqueku.com/t/…",
"cancel_reason": null,
"failure_reason": null,
"timestamps": {
"created_at": "2026-11-04T18:00:00Z",
"assigned_at": null,
"accepted_at": null,
"picked_up_at": null,
"delivered_at": null,
"failed_at": null,
"cancelled_at": null
},
"simulated": true
}
GET/api/v1/pricing
Tarifas y planes
Tarifas vigentes por tipo de servicio (sin IVA) y planes de suscripción.
Respuesta
{
"currency": "EUR",
"vat_rate": 0.21,
"prices_exclude_vat": true,
"tariffs": [
{
"service_type": "immediate",
"city_name": null,
"base_cents": 390,
"per_km_cents": 60,
"included_km": 2,
"min_cents": 390,
"max_km": 12
}
],
"plans": [
{
"code": "starter",
"name": "Starter",
"price_month_cents": 0
}
]
}
GET/api/v1/businesses/{id}/deliveriespermiso deliveries:read
Repartos de tu empresa
Usa me o tu id de empresa. ?from=&to= (YYYY-MM-DD), limit.
Respuesta
{
"business_id": "…",
"data": [
{
"id": "8f3e2a1c-5b7d-4c1e-9a2f-1d2c3b4a5e6f",
"code": "PKD-7F3K2Q",
"environment": "sandbox",
"external_reference": "PEDIDO-1042",
"status": "PENDING_ASSIGNMENT",
"service_type": "immediate",
"scheduled_at": null,
"pickup": {
"name": "Tienda Centro",
"phone": "+34600000001",
"address": "Calle Mesones 5, 18001 Granada",
"lat": 37.1752,
"lng": -3.6001,
"instructions": "Portal 2, 3ºB"
},
"dropoff": {
"name": "Pedro García",
"phone": "+34600000002",
"address": "Calle Recogidas 20, 18005 Granada",
"lat": 37.171,
"lng": -3.6045,
"instructions": "Portal 2, 3ºB"
},
"extra_stops": [],
"package": {
"size": "small",
"weight_kg": 2,
"description": "Ropa",
"fragile": false
},
"distance_m": 791,
"estimated_duration_s": 142,
"price": {
"amount_cents": 390,
"tax_cents": 82,
"total_cents": 472,
"currency": "EUR"
},
"courier_assigned": false,
"tracking_url": "https://delivery.piqueku.com/t/…",
"cancel_reason": null,
"failure_reason": null,
"timestamps": {
"created_at": "2026-11-04T18:00:00Z",
"assigned_at": null,
"accepted_at": null,
"picked_up_at": null,
"delivered_at": null,
"failed_at": null,
"cancelled_at": null
}
}
]
}
GET/api/v1/organizations/{id}/status
Estado de una organización
Estado operativo agregado de una organización logística (cooperativa o empresa).
Respuesta
{
"id": "…",
"name": "…",
"kind": "cooperative",
"legal_status": "validated",
"city": "granada",
"couriers_active": 12,
"couriers_available": 5,
"operating": true
}
POST/api/v1/webhookspermiso webhooks:write
Suscribir webhook
URL https pública (no se admiten direcciones privadas). El secreto de firma se devuelve una sola vez. GET lista tus webhooks; DELETE /api/v1/webhooks/{id} lo borra.
Petición
{
"url": "https://tu-web.com/webhooks/piqueku",
"events": [
"delivery.created",
"delivery.picked_up",
"delivery.delivered",
"delivery.failed",
"delivery.cancelled"
]
}
Respuesta
{
"id": "…",
"url": "https://tu-web.com/webhooks/piqueku",
"events": [
"delivery.created"
],
"environment": "sandbox",
"secret": "whsec_…",
"created_at": "…"
}
GET/api/v1/usagepermiso usage:read
Uso del mes
Peticiones de API y repartos del mes frente a los límites de tu plan.
Respuesta
{
"period_start": "2026-11-01",
"plan": {
"code": "business",
"name": "Business"
},
"api_requests": {
"total": 1830,
"by_status": {
"2xx": 1790,
"4xx": 40
},
"limit": 50000
},
"deliveries": {
"live": 212,
"sandbox": 37,
"live_limit": 1000
}
}
Consola de pruebas
Prueba la API con tu clave de pruebas (pkd_test_). La petición sale de tu navegador directamente a la API; la clave no se guarda.