Bepudo Integration — Sandbox Environment
Reference document for integrating with the Bepudo API in the test environment.
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 | documented → checking_pending → at_pudo → collected |
ath |
At Home | Home delivery (may go through a PUDO) | documented → checking_pending → at_pudo → collected |
rtn |
Return | Customer returns an item via PUDO | documented → at_pudo → collected |
ndr |
Non-Delivery Return | Created automatically by Bepudo when an at_pudo order is cancelled |
documented → at_pudo → collected |
c2c |
Customer to Customer | Delivery between customers | at_pudo → collected |
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
pudoUuidof 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-codeexists to return the current code +expiresIn. IfexpiresIn < 5s, wait for the next rotation before callingPOST /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
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
securityTokenis 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
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 |
documented → checking_pending |
B2C (ooh/ath) |
availableForCollection |
checking_pending → at_pudo |
B2C |
collected |
at_pudo → collected |
B2C |
expired |
documented → (removed after 5 days) |
B2C |
notReceived |
checking_pending → not_received |
B2C |
nonDeliveryReturn |
at_pudo → cancelled/rejected |
B2C |
availableForPickUp |
documented → at_pudo |
C2B (rtn/ndr) |
pickedUp |
at_pudo → collected |
C2B |
lost / found |
various | both |