openapi: 3.0.3 info: title: Waypost API version: "1.0.0" description: | Tracking multi-corriere per l'Italia. Autenticazione con `Authorization: Bearer `. Le chiavi `wp_test_…` usano solo il corriere `sandbox` (numeri `SBXDELIVERED`, `SBXTRANSIT`, `SBXEXCEPTION`, `SBXPICKUP`) e sono gratuite. Webhook: ogni invio porta l'header `X-Signature: t=,v1=`; rifiutate le firme più vecchie di 5 minuti e deduplicate con `event_id`. servers: - url: https://api.waypost.it security: - apiKey: [] - session: [] paths: /auth/signup: post: summary: Registrazione dal portale (piano Free) e apertura della sessione security: [] parameters: [{ $ref: "#/components/parameters/Client" }] requestBody: required: true content: application/json: schema: type: object required: [name, email, password] properties: name: { type: string } email: { type: string, format: email } password: { type: string, minLength: 8, maxLength: 72 } responses: "201": { description: Registrato, cookie di sessione impostato } "409": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } /auth/login: post: summary: Accesso al portale security: [] parameters: [{ $ref: "#/components/parameters/Client" }] requestBody: required: true content: application/json: schema: type: object required: [email, password] properties: email: { type: string } password: { type: string } responses: "200": { description: Accesso eseguito, cookie di sessione impostato } "401": { $ref: "#/components/responses/Error" } /auth/logout: post: summary: Chiude la sessione security: [] responses: "204": { description: Uscito } /auth/me: get: summary: Utente, account e piano della sessione security: [{ session: [] }] responses: "200": description: Sessione content: { application/json: { schema: { $ref: "#/components/schemas/Me" } } } "401": { $ref: "#/components/responses/Error" } /auth/verify-email: post: summary: Conferma l'email con il link ricevuto (funziona anche senza sessione) security: [] parameters: [{ $ref: "#/components/parameters/Client" }] requestBody: required: true content: application/json: schema: type: object required: [token] properties: token: { type: string } responses: "200": description: Email confermata content: application/json: schema: type: object properties: status: { type: string } email: { type: string } "410": { $ref: "#/components/responses/Error" } /auth/verify-email/resend: post: summary: Invia di nuovo il link di conferma (al massimo uno ogni 10 minuti) security: [{ session: [] }] parameters: [{ $ref: "#/components/parameters/Client" }] responses: "200": description: Inviato, o email già confermata content: application/json: schema: type: object properties: status: { type: string, enum: [sent, already_verified] } email: { type: string } /v1/plans: get: summary: Listino dei piani responses: "200": description: Piani content: application/json: schema: type: object properties: data: { type: array, items: { $ref: "#/components/schemas/Plan" } } /v1/billing: get: summary: Abbonamento dell'account (solo dal portale) security: [{ session: [] }] responses: "200": description: Stato dell'abbonamento content: { application/json: { schema: { $ref: "#/components/schemas/Billing" } } } /v1/billing/checkout: post: summary: Avvia Stripe Checkout per un piano; restituisce l'URL di pagamento security: [{ session: [] }] requestBody: required: true content: application/json: schema: type: object required: [plan] properties: plan: { type: string, enum: [base, plus, starter, business, pro] } interval: { type: string, enum: [month, year], default: month } responses: "200": description: URL di Stripe Checkout content: { application/json: { schema: { type: object, properties: { url: { type: string } } } } } "409": { $ref: "#/components/responses/Error" } "503": { $ref: "#/components/responses/Error" } /v1/billing/portal: post: summary: Apre il Customer Portal di Stripe (cambio piano, carta, fatture, disdetta) security: [{ session: [] }] responses: "200": description: URL del portale Stripe content: { application/json: { schema: { type: object, properties: { url: { type: string } } } } } "409": { $ref: "#/components/responses/Error" } /v1/api-keys: get: summary: Chiavi API dell'account (solo dal portale) security: [{ session: [] }] responses: "200": description: Chiavi content: application/json: schema: type: object properties: data: { type: array, items: { $ref: "#/components/schemas/APIKey" } } post: summary: Crea una chiave; il valore completo si vede solo ora security: [{ session: [] }] requestBody: required: true content: application/json: schema: type: object properties: name: { type: string, maxLength: 100 } mode: { type: string, enum: [live, test], default: live } responses: "201": description: Creata content: { application/json: { schema: { $ref: "#/components/schemas/APIKey" } } } "402": { $ref: "#/components/responses/Error" } "403": { $ref: "#/components/responses/Error" } # email_not_verified /v1/api-keys/{id}: delete: summary: Revoca una chiave security: [{ session: [] }] parameters: - { name: id, in: path, required: true, schema: { type: string, format: uuid } } responses: "204": { description: Revocata } "404": { $ref: "#/components/responses/Error" } /v1/webhooks/{id}/deliveries: get: summary: Ultimi 50 invii di un endpoint parameters: - { name: id, in: path, required: true, schema: { type: string, format: uuid } } responses: "200": description: Storico content: application/json: schema: type: object properties: data: { type: array, items: { $ref: "#/components/schemas/Delivery" } } /v1/trackings: post: summary: Registra fino a 100 numeri description: | Atomico: o vengono registrati tutti, o nessuno. Un numero già attivo restituisce il tracking esistente (`created: false`) senza consumare quota. Se `carrier` manca, il corriere viene rilevato. parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: type: object required: [trackings] properties: trackings: type: array minItems: 1 maxItems: 100 items: { $ref: "#/components/schemas/NewTracking" } responses: "201": { $ref: "#/components/responses/Registered" } "200": { $ref: "#/components/responses/Registered" } "402": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } "429": { $ref: "#/components/responses/Error" } get: summary: Elenco dei tracking, dal più recente parameters: - { name: status, in: query, schema: { $ref: "#/components/schemas/Status" } } - { name: carrier, in: query, schema: { type: string } } - { name: active, in: query, schema: { type: boolean } } - { name: created_after, in: query, schema: { type: string, format: date-time } } - { name: created_before, in: query, schema: { type: string, format: date-time } } - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } } - { name: cursor, in: query, description: "next_cursor della pagina precedente", schema: { type: string } } responses: "200": description: Pagina di tracking content: application/json: schema: type: object properties: data: { type: array, items: { $ref: "#/components/schemas/Tracking" } } next_cursor: { type: string, nullable: true } /v1/trackings/{id}: parameters: - { name: id, in: path, required: true, schema: { type: string, format: uuid } } get: summary: Stato e cronologia eventi responses: "200": description: Tracking con eventi content: { application/json: { schema: { $ref: "#/components/schemas/Tracking" } } } "404": { $ref: "#/components/responses/Error" } delete: summary: Smette di monitorare (libera lo slot sui piani a pacchi attivi) responses: "204": { description: Fermato } "404": { $ref: "#/components/responses/Error" } /v1/detect: post: summary: Corrieri candidati per un numero, senza registrarlo requestBody: required: true content: application/json: schema: { type: object, required: [number], properties: { number: { type: string } } } responses: "200": description: Candidati ordinati per punteggio content: application/json: schema: type: object properties: number: { type: string } candidates: type: array items: type: object properties: carrier: { type: string } score: { type: number } reason: { type: string } supported: { type: boolean } tracking_url: { type: string } /v1/carriers: get: summary: Corrieri conosciuti e loro capacità responses: "200": description: Catalogo content: application/json: schema: type: object properties: data: type: array items: type: object properties: code: { type: string } name: { type: string } supported: { type: boolean } source: { type: string, enum: [api, byoc, scrape, sandbox] } batch_size: { type: integer } push: { type: boolean } /v1/webhooks: post: summary: Aggiunge un endpoint di notifica parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: type: object required: [url] properties: url: { type: string, format: uri, description: "https, indirizzo pubblico" } events: type: array description: 'Vuoto o ["*"] per tutti' items: { type: string, enum: ["*", tracking.updated, tracking.delivered, tracking.exception, tracking.expired] } responses: "201": description: Creato; il segreto di firma è incluso content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEndpoint" } } } "402": { $ref: "#/components/responses/Error" } "422": { $ref: "#/components/responses/Error" } get: summary: Elenco endpoint (senza segreto) responses: "200": description: Endpoint content: application/json: schema: type: object properties: data: { type: array, items: { $ref: "#/components/schemas/WebhookEndpoint" } } /v1/webhooks/{id}: delete: summary: Rimuove un endpoint parameters: - { name: id, in: path, required: true, schema: { type: string, format: uuid } } responses: "204": { description: Rimosso } "404": { $ref: "#/components/responses/Error" } /v1/webhooks/{id}/test: post: summary: Invia un evento di test parameters: - { name: id, in: path, required: true, schema: { type: string, format: uuid } } responses: "202": description: In coda content: application/json: schema: { type: object, properties: { event_id: { type: string, format: uuid } } } /v1/usage: get: summary: Consumo rispetto al piano description: | Sui piani a pagamento `used` conta i pacchi attivi; sul piano Free conta i numeri registrati nel mese solare (UTC), anche se poi fermati o consegnati. responses: "200": description: Consumo content: application/json: schema: type: object properties: livemode: { type: boolean } plan: { $ref: "#/components/schemas/Plan" } quota: { type: string, enum: [active, monthly] } limit: { type: integer } used: { type: integer } active_trackings: { type: integer } created_this_month: { type: integer } period_start: { type: string, format: date-time } period_end: { type: string, format: date-time } components: securitySchemes: apiKey: { type: http, scheme: bearer } session: type: apiKey in: cookie name: waypost_session description: Sessione del portale. Le richieste che modificano dati devono avere l'header X-Waypost-Client=portal. parameters: Client: name: X-Waypost-Client in: header required: true schema: { type: string, enum: [portal] } IdempotencyKey: name: Idempotency-Key in: header description: La stessa richiesta ripetuta con la stessa chiave restituisce la risposta originale (24 h). schema: { type: string, maxLength: 255 } responses: Registered: description: Tracking registrati (201 se almeno uno è nuovo) content: application/json: schema: type: object properties: data: type: array items: allOf: - $ref: "#/components/schemas/Tracking" - type: object properties: { created: { type: boolean } } Error: description: Errore content: application/json: schema: type: object properties: error: type: object properties: code: { type: string, example: plan_limit_reached } message: { type: string } details: {} schemas: Status: type: string enum: [pending, info_received, in_transit, out_for_delivery, available_for_pickup, exception, failed_attempt, delivered, returned, expired] NewTracking: type: object required: [number] properties: number: { type: string, description: "Spazi e trattini vengono rimossi" } carrier: { type: string, description: "Salta il rilevamento automatico" } order_ref: { type: string, maxLength: 200 } metadata: { type: object, description: "Oggetto JSON fino a 4 KB" } Tracking: type: object properties: id: { type: string, format: uuid } object: { type: string, example: tracking } livemode: { type: boolean } number: { type: string } carrier: { type: string, nullable: true, description: "null finché il rilevamento non è confermato" } carrier_candidates: { type: array, items: { type: string } } tracking_url: { type: string, description: "Pagina pubblica di tracking del corriere, da aprire nel browser" } check_error: { type: string, description: "Perché l'ultimo controllo non è riuscito (es. pagina del corriere cambiata); assente se è andato bene" } status: { $ref: "#/components/schemas/Status" } status_label: { type: string, example: In transito } substatus: { type: string, example: giacenza } active: { type: boolean } order_ref: { type: string } metadata: { type: object } last_event_at: { type: string, format: date-time, nullable: true } next_check_at: { type: string, format: date-time, nullable: true } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } events: type: array items: type: object properties: occurred_at: { type: string, format: date-time } status: { allOf: [{ $ref: "#/components/schemas/Status" }], nullable: true } status_label: { type: string } substatus: { type: string } carrier_code: { type: string } description: { type: string } location: { type: string } country_code: { type: string } WebhookEndpoint: type: object properties: id: { type: string, format: uuid } object: { type: string, example: webhook_endpoint } url: { type: string } events: { type: array, items: { type: string } } active: { type: boolean } disabled_at: { type: string, format: date-time, nullable: true } created_at: { type: string, format: date-time } secret: { type: string, description: "Solo alla creazione" } Plan: type: object properties: code: { type: string } name: { type: string } price_eur_cents: { type: integer } quota: { type: string, enum: [active, monthly] } limit: { type: integer } api_keys: { type: integer, description: "0 = illimitate" } webhooks: { type: integer, description: "0 = illimitati" } requests_per_minute: { type: integer } byoc: { type: boolean } contact_sales: { type: boolean, description: "No self-service price: write to us." } Me: type: object properties: user: type: object properties: id: { type: string, format: uuid } email: { type: string } name: { type: string } role: { type: string } superuser: { type: boolean, description: "Runs the service: sees the admin dashboard (portal only)." } email_verified: { type: boolean, description: "The email is confirmed; needed to create API keys." } account: type: object properties: id: { type: string, format: uuid } name: { type: string } subscription_status: { type: string } plan: { $ref: "#/components/schemas/Plan" } APIKey: type: object properties: id: { type: string, format: uuid } name: { type: string } prefix: { type: string } livemode: { type: boolean } last_used_at: { type: string, format: date-time, nullable: true } created_at: { type: string, format: date-time } key: { type: string, description: "Solo alla creazione" } Delivery: type: object properties: id: { type: string, format: uuid } event_id: { type: string, format: uuid } event_type: { type: string } status: { type: string, enum: [pending, succeeded, failed] } attempts: { type: integer } last_status_code: { type: integer, nullable: true } last_error: { type: string } payload: { type: object } created_at: { type: string, format: date-time } delivered_at: { type: string, format: date-time, nullable: true } next_attempt_at: { type: string, format: date-time, nullable: true } Billing: type: object properties: enabled: { type: boolean, description: "Stripe configurato sul server" } plan: { $ref: "#/components/schemas/Plan" } status: { type: string, description: "active, past_due, unpaid, incomplete, canceled" } interval: { type: string, enum: [month, year] } has_subscription: { type: boolean } current_period_end: { type: string, format: date-time, nullable: true } read_only: { type: boolean }