Skip to content

Bepudo Integration — Sandbox Environment

Reference document for integrating with the Bepudo API in the test environment.

Download Postman Collection


What is a PUDO?

PUDO = Pick-Up Drop-Off — a physical location (shop, grocery store, tobacconist, etc.) that acts as a parcel drop-off/pick-up point. These are not automated lockers — they are staffed shops that operate independently within the Bepudo network.

Actor What How
Us (Dingoo) We create the order, driver scans it in Via Bepudo API in our app
PUDO (shop) Confirms receipt of the parcels Via Bepudo's own internal system
Customer Picks up using the securityToken At the shop counter

Documentation Access

Resource Detail
Swagger / OpenAPI https://api-sandbox.bepudo.com/
Username client
Password Esto es un puntazo
API Key (sandbox) bepudo_test_2fb59f5bc13a141a5617655a3c673f07311807c2d4ca94c38129ca6ac244a375_73a5533b

⚠️ The API Key above is sandbox-only. New credentials will be issued for production.


Order Types (orderType)

Type Name Use case Flow
ooh Out of Home Failed home delivery → redirect to PUDO documentedchecking_pendingat_pudocollected
ath At Home Home delivery (may go through a PUDO) documentedchecking_pendingat_pudocollected
rtn Return Customer returns an item via PUDO documentedat_pudocollected
ndr Non-Delivery Return Created automatically by Bepudo when an at_pudo order is cancelled documentedat_pudocollected
c2c Customer to Customer Delivery between customers at_pudocollected

For our use case (failed delivery → PUDO) the type is ooh.


Available Test PUDOs

graph TD
    subgraph Lisboa ["📍 Lisbon"]
        PUDO1["🏪 Mercearia Central Baixa<br/>UUID: 7b2e662a-579c-43d3-b3c7-edf8438090c6<br/>Postal: 1100-053<br/>Coords: 38.7106, -9.1387<br/>Hours: Mon–Sat 09:00–20:00"]
        PUDO2["🏪 Tabacaria Alvalade<br/>Postal: 1700-239<br/>Coords: 38.7536, -9.1448<br/>Hours: Mon–Sat 08:00–19:30"]
    end

    subgraph Porto ["📍 Porto"]
        PUDO3["🏪 Padaria Bolhão<br/>Postal: 4000-211<br/>Coords: 41.1496, -8.6063<br/>Hours: Mon–Sat 07:30–19:00"]
    end

    PUDO1 -. "~5 km" .- PUDO2

Searching PUDOs — GET /pudos

The endpoint always requires a filter — without one it returns a 400 error.

flowchart TD
    A[GET /pudos] --> B{Filter type?}

    B -->|By postal code| C["?postalCode=4000-211"]
    B -->|By coordinates + radius| D["?latitude=...&longitude=...&radius=km (max 10)"]
    B -->|No filter| E["❌ 400 Bad Request"]

    C --> F["Returns: Padaria Bolhão"]

    D --> G{Chosen radius}
    G -->|radius=1 km| H["Returns: Mercearia Central Baixa (only)"]
    G -->|radius=8 km| I["Returns: Mercearia Central Baixa + Tabacaria Alvalade"]

The response includes the pudoUuid of each PUDO, required for subsequent endpoints.


Authentication — Two token levels

flowchart LR
    A["🔑 API Key\n(permanent)"]
    B["POST /auth/token\nHeader: X-API-Key"]
    C["JWT Bearer Token\n⏱ valid 1 hour"]
    D["POST /session\nBody: pudoUuid + totpCode"]
    E["Session Token\n⏱ valid 5 minutes"]

    A --> B --> C
    C --> D --> E

    style E fill:#f0ad4e,color:#000
Token Obtained via Validity Used for
JWT Bearer POST /auth/token with API Key 1 hour All endpoints
Session Token POST /session with PUDO TOTP code 5 minutes Physical parcel scan

The TOTP — Proof of physical presence

Each PUDO location has a physical device (tablet/screen) that displays an 8-digit code rotating every 30 seconds. It works like 2FA — proving the driver is physically at the shop.

sequenceDiagram
    participant Loja as 🏪 Shop Screen (PUDO)
    participant Driver as 🚴 Driver
    participant App as 📱 Dingoo App
    participant API as Bepudo API

    Loja-->>Driver: Shows code "12345678" (30s)
    Driver->>App: Types the code into the app
    App->>API: POST /session { pudoUuid, totpCode: "12345678" }
    API-->>App: { sessionToken, expiresIn: 300 }
    Note over App: Session Token valid for 5 minutes

Sandbox: there is no physical access to the shop, so GET /pudos/{pudoUuid}/totp-code exists to return the current code + expiresIn. If expiresIn < 5s, wait for the next rotation before calling POST /session.


Full Flow — Failed home delivery → PUDO

sequenceDiagram
    participant App as 📱 Dingoo App
    participant API as Bepudo API
    participant Loja as 🏪 PUDO Shop

    App->>API: POST /auth/token (X-API-Key)
    API-->>App: JWT (1h)

    App->>API: GET /pudos?latitude=...&longitude=...&radius=5
    API-->>App: List of nearby PUDOs + pudoUuids

    App->>API: POST /orders { packageCode, pudoUuid, orderType:"ooh", securityToken, ... }
    API-->>App: Order created → status: "documented"

    Note over App,Loja: Driver arrives at the shop and reads the screen code

    App->>API: POST /session { pudoUuid, totpCode: "12345678" }
    API-->>App: Session Token (5 min)

    App->>API: POST /orders/{packageCode}/scan (Bearer + X-Session-Token)
    API-->>App: documented → checking_pending ✅

    Note over Loja: Shop staff confirms receipt in Bepudo system
    Loja->>API: (Bepudo internal system)
    API-->>Loja: checking_pending → at_pudo

    Note over App: Customer picks up using the securityToken

The 4 endpoints in practice

1. Get JWT

POST /auth/token
Header: X-API-Key: bepudo_test_2fb59f5bc13a...

2. Create the order

POST /orders
Header: Authorization: Bearer eyJ...

{
  "orders": [{
    "packageCode": "PKG-DINGOO-001",
    "pudoUuid": "7b2e662a-579c-43d3-b3c7-edf8438090c6",
    "orderType": "ooh",
    "customerName": "Customer Name",
    "dimensions": { "length": 10, "width": 20, "height": 30, "weight": 1000 },
    "securityToken": { "value": "CODE-FOR-THE-CUSTOMER", "isManual": true }
  }]
}

The securityToken is the code the customer uses to collect the parcel at the shop — different from the driver's TOTP.

3. Create session (driver read the code from the shop screen)

POST /session
Header: Authorization: Bearer eyJ...

{ "pudoUuid": "7b2e662a-579c-43d3-b3c7-edf8438090c6", "totpCode": "12345678" }

4. Scan — confirm physical drop-off

POST /orders/PKG-DINGOO-001/scan
Headers:
  Authorization: Bearer eyJ...
  X-Session-Token: eyJ...

Order Lifecycle (ooh / ath)

stateDiagram-v2
    direction LR

    [*] --> documented : POST /orders\n(Dingoo creates order)
    documented --> checking_pending : POST /orders/{code}/scan\n(driver drops off at shop)
    checking_pending --> at_pudo : Shop staff confirms\n(Bepudo internal system)
    documented --> at_pudo : PUDO confirms directly\n(no prior scan)
    at_pudo --> collected : Customer collects\n(with securityToken)
    at_pudo --> cancelled : DELETE /orders/{code}
    cancelled --> [*]
    collected --> [*]

Return Lifecycle (rtn / ndr)

stateDiagram-v2
    direction LR

    [*] --> documented : POST /orders\n(Dingoo creates return)
    documented --> at_pudo : Customer drops off at shop
    at_pudo --> collected : Driver collects
    collected --> [*]

Sandbox Helper Endpoints

These endpoints do not exist in production and allow bulk simulation of physical steps, affecting all orders in the source state.

flowchart LR
    A["POST /orders/set-at-pudo"]:::helper -->|checking_pending → at_pudo| B["Simulates shop confirming receipt"]
    C["POST /orders/rtn-set-at-pudo"]:::helper -->|documented → at_pudo| D["Simulates customer dropping off a return"]
    E["POST /orders/set-collected"]:::helper -->|at_pudo → collected| F["Simulates final collection"]

    B & D & F --> G["📬 Webhooks fired (same as production)"]

    classDef helper fill:#f0ad4e,color:#000,stroke:#d68910

Each call returns the list of affected packageCodes. Multiple orders are processed at once.


Webhook Events

Event Transition Model
droppedAtPudo documentedchecking_pending B2C (ooh/ath)
availableForCollection checking_pendingat_pudo B2C
collected at_pudocollected B2C
expired documented(removed after 5 days) B2C
notReceived checking_pendingnot_received B2C
nonDeliveryReturn at_pudocancelled/rejected B2C
availableForPickUp documentedat_pudo C2B (rtn/ndr)
pickedUp at_pudocollected C2B
lost / found various both