{
  "openapi": "3.1.0",
  "info": {
    "title": "LastFolk API",
    "version": "1.0.0",
    "description": "Local same-day delivery for any storefront: quote, dispatch, track and cancel deliveries on your own Uber Direct account through one API.\n\nv1 is FROZEN: every route, request/response field, header, and error code in this document is a permanent promise. Changes are additive-only (new optional fields, new endpoints, new enum values where marked additive); anything breaking would ship as a side-by-side v2. See the versioning policy at https://lastfolk.com/developers/docs#versioning.\n\nAuthentication: `Authorization: Bearer lf_test_...` (sandbox) or `Bearer lf_live_...` (production). The key decides the environment — there is no environment parameter anywhere. `lf_test_` keys operate against your sandbox Uber Direct account and `lf_live_` keys against your production one; a mismatch refuses with `api_key_environment_mismatch` before anything is created or charged.\n\nThe base path `/v1` also composes with the future canonical host `api.lastfolk.com`; both path shapes are accepted by the server.",
    "contact": {
      "url": "https://lastfolk.com/developers"
    }
  },
  "servers": [
    {
      "url": "https://koajaocfoybsnnebixsu.supabase.co/functions/v1/api-v1",
      "description": "Production edge (current base URL; api.lastfolk.com will be added as an alias without breaking this one)"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/v1/whoami": {
      "get": {
        "operationId": "whoami",
        "summary": "Identity echo — which tenant and environment this key resolves to",
        "responses": {
          "200": {
            "description": "The authenticated identity.",
            "headers": {
              "$ref": "#/components/headers/RateLimitHeaders"
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "tenant_id",
                    "environment",
                    "scopes",
                    "key",
                    "request_id"
                  ],
                  "properties": {
                    "tenant_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "environment": {
                      "$ref": "#/components/schemas/Environment"
                    },
                    "scopes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Reserved. Always [] today; scoped keys are a future additive feature."
                    },
                    "key": {
                      "type": "object",
                      "required": [
                        "prefix",
                        "last4"
                      ],
                      "properties": {
                        "prefix": {
                          "type": "string",
                          "enum": [
                            "lf_test_",
                            "lf_live_"
                          ]
                        },
                        "last4": {
                          "type": "string"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/environment": {
      "get": {
        "operationId": "checkEnvironment",
        "summary": "Preflight — does this key's environment match the connected provider account?",
        "description": "The same guard POST /v1/quotes and POST /v1/deliveries run, exposed as a read so an integration can verify its wiring before dispatching anything. A mismatch is `api_key_environment_mismatch` (422); no connected provider account is `provider_environment_unknown` (422).",
        "responses": {
          "200": {
            "description": "Key and provider account agree.",
            "headers": {
              "$ref": "#/components/headers/RateLimitHeaders"
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "environment",
                    "key_environment",
                    "provider_environment",
                    "match",
                    "request_id"
                  ],
                  "properties": {
                    "environment": {
                      "$ref": "#/components/schemas/Environment"
                    },
                    "key_environment": {
                      "$ref": "#/components/schemas/Environment"
                    },
                    "provider_environment": {
                      "$ref": "#/components/schemas/Environment"
                    },
                    "match": {
                      "type": "boolean",
                      "const": true
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/quotes": {
      "post": {
        "operationId": "createQuote",
        "summary": "Price a pickup→dropoff pair",
        "description": "Read-only: reserves nothing, charges nothing, dispatches nothing. The returned quote is single-use and expires at `expires_at`; dispatch it with POST /v1/deliveries.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "pickup",
                  "dropoff"
                ],
                "properties": {
                  "pickup": {
                    "$ref": "#/components/schemas/QuoteEndpoint"
                  },
                  "dropoff": {
                    "$ref": "#/components/schemas/QuoteEndpoint"
                  },
                  "manifest": {
                    "$ref": "#/components/schemas/DeclaredManifest"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The quote. `price.amount` is a DECIMAL STRING in `price.currency` — parse it with a decimal-safe library, never a float.",
            "headers": {
              "$ref": "#/components/headers/RateLimitHeaders"
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/deliveries": {
      "get": {
        "operationId": "listDeliveries",
        "summary": "List your deliveries",
        "description": "Newest first. Read-only: no provider round-trip, no money. Scoped to your own tenant and to your key's environment — a delivery you cannot see is simply absent, and an empty list is indistinguishable from a filtered-out one.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Repeatable. An unknown value is refused with invalid_request rather than ignored.",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/DeliveryStatus"
              }
            }
          },
          {
            "name": "external_order_id",
            "in": "query",
            "required": false,
            "description": "Your own order id, as sent on create. NOT unique — two dispatches may carry the same one — so this filters and always returns an array.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "created_after",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "created_before",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "An opaque cursor from a previous response's next_cursor.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of deliveries, newest first.",
            "headers": {
              "$ref": "#/components/headers/RateLimitHeaders"
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeliveryList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "createDelivery",
        "summary": "Dispatch a delivery against a quote",
        "description": "The money path. `Idempotency-Key` is REQUIRED: one key = one delivery. A retry with the same key and body replays the original result (`Idempotent-Replay: true` header) — it can never dispatch a second courier or take a second charge. The same key with a DIFFERENT body refuses with `idempotency_key_reused`. Quotes are single-use (`quote_already_used`) and must not be expired (`quote_expired`). `dropoff.phone` must be a real, reachable E.164 number — placeholders are refused.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "A unique value per delivery attempt (an order id is a good choice)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "quote_id",
                  "dropoff"
                ],
                "properties": {
                  "quote_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "From POST /v1/quotes. Single-use."
                  },
                  "external_order_id": {
                    "type": "string",
                    "description": "Your own order reference. Echoed on the delivery and in webhook events."
                  },
                  "dropoff": {
                    "type": "object",
                    "required": [
                      "name",
                      "phone"
                    ],
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "phone": {
                        "type": "string",
                        "description": "E.164. The courier's contact for the recipient; placeholder/reserved numbers are refused."
                      }
                    }
                  },
                  "customer_email": {
                    "type": "string"
                  },
                  "pickup_notes": {
                    "type": "string"
                  },
                  "manifest": {
                    "$ref": "#/components/schemas/DeclaredManifest"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The delivery was dispatched. (Success is 200, not 201 — frozen.)",
            "headers": {
              "$ref": "#/components/headers/RateLimitHeaders"
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Delivery"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "402": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/deliveries/{delivery_id}": {
      "get": {
        "operationId": "getDelivery",
        "summary": "The persisted state of a delivery",
        "description": "Served from LastFolk's store, which courier webhooks keep current — no provider round-trip, so it is fast and always available. A delivery belonging to another account, or to the other environment, is indistinguishable from a missing one (`delivery_not_found`).",
        "parameters": [
          {
            "$ref": "#/components/parameters/DeliveryId"
          }
        ],
        "responses": {
          "200": {
            "description": "The delivery detail.",
            "headers": {
              "$ref": "#/components/headers/RateLimitHeaders"
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeliveryDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/deliveries/{delivery_id}/cancel": {
      "post": {
        "operationId": "cancelDelivery",
        "summary": "Cancel a delivery still inside the cancellation window",
        "description": "Cancellable while status is `pending` or `pickup` (the courier has not collected the order), re-checked server-side. Cancelling an already-cancelled delivery is an idempotent SUCCESS (`changed: false`). Outside the window: `delivery_not_cancellable` (409). Cancelling moves no money in the same request; any payment hold resolves through the normal billing flow.",
        "parameters": [
          {
            "$ref": "#/components/parameters/DeliveryId"
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled (or already was).",
            "headers": {
              "$ref": "#/components/headers/RateLimitHeaders"
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "delivery_id",
                    "status",
                    "changed",
                    "request_id"
                  ],
                  "properties": {
                    "delivery_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "status": {
                      "type": "string",
                      "const": "cancelled"
                    },
                    "changed": {
                      "type": "boolean",
                      "description": "false when the delivery was already cancelled."
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/billing-profile": {
      "get": {
        "operationId": "getBillingProfile",
        "summary": "Read the account's fiscal identity",
        "responses": {
          "200": {
            "description": "The billing profile — exactly seven fields, each string or null.",
            "headers": {
              "$ref": "#/components/headers/RateLimitHeaders"
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "billing_profile",
                    "request_id"
                  ],
                  "properties": {
                    "billing_profile": {
                      "$ref": "#/components/schemas/BillingProfile"
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "put": {
        "operationId": "updateBillingProfile",
        "summary": "Update the fiscal identity (non-destructive merge)",
        "description": "A field that is ABSENT or an EMPTY string keeps its stored value; send the literal JSON null to clear one — a partial update can never wipe the rest of the profile. `tax_id` is validated against the RESULTING `billing_address_country` (post-merge); a failure refuses the WHOLE request atomically. Changes propagate to your billing records; the outcome is reported in `stripe_sync.status`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BillingProfileUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored profile after the merge (re-read from the database).",
            "headers": {
              "$ref": "#/components/headers/RateLimitHeaders"
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "billing_profile",
                    "changed",
                    "kept_existing",
                    "stripe_sync",
                    "request_id"
                  ],
                  "properties": {
                    "billing_profile": {
                      "$ref": "#/components/schemas/BillingProfile"
                    },
                    "changed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Fields this request changed."
                    },
                    "kept_existing": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Fields sent empty whose stored values were kept (the non-destructive rule)."
                    },
                    "stripe_sync": {
                      "type": "object",
                      "required": [
                        "status"
                      ],
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "not_invoked",
                            "skipped",
                            "synced",
                            "no_stripe_customer",
                            "failed"
                          ]
                        },
                        "fields": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "reason": {
                          "type": "string"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key issued in your LastFolk dashboard: `lf_test_` + 48 hex (sandbox) or `lf_live_` + 48 hex (production). Shown exactly once at issuance."
      }
    },
    "parameters": {
      "DeliveryId": {
        "name": "delivery_id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "The `delivery_id` returned by POST /v1/deliveries."
      }
    },
    "headers": {
      "RateLimitHeaders": {
        "description": "Every authenticated response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the fixed window resets). 429 responses add Retry-After.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "Error": {
        "description": "The uniform error envelope. Program against `error.code` (frozen — see ErrorCode); `error.message` is human-readable guidance and may be refined over time. `error.request_id` identifies the request for support.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests in the current window. Honor Retry-After.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds until the window resets."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Environment": {
        "type": "string",
        "enum": [
          "sandbox",
          "production"
        ]
      },
      "ErrorCode": {
        "type": "string",
        "description": "The complete, frozen public error vocabulary. An unrecognized internal condition always surfaces as internal_error — no other code can ever appear.",
        "enum": [
          "missing_authorization",
          "malformed_authorization",
          "invalid_api_key",
          "revoked_api_key",
          "api_key_environment_mismatch",
          "provider_environment_unknown",
          "no_coverage",
          "provider_unavailable",
          "provider_not_configured",
          "quote_not_found",
          "quote_expired",
          "quote_already_used",
          "phone_not_verified",
          "billing_blocked",
          "payment_authorization_failed",
          "dispatch_failed",
          "idempotency_key_required",
          "idempotency_key_in_progress",
          "multi_location_addon_required",
          "delivery_not_found",
          "delivery_not_cancellable",
          "not_found",
          "method_not_allowed",
          "invalid_request",
          "idempotency_key_reused",
          "rate_limited",
          "internal_error"
        ]
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "$ref": "#/components/schemas/ErrorCode"
              },
              "message": {
                "type": "string"
              },
              "request_id": {
                "type": "string",
                "format": "uuid"
              },
              "details": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        }
      },
      "QuoteEndpoint": {
        "type": "object",
        "required": [
          "address",
          "street_address",
          "city",
          "country"
        ],
        "properties": {
          "address": {
            "type": "string",
            "description": "The full human-readable line. Persisted and shown in LastFolk surfaces."
          },
          "street_address": {
            "type": "string",
            "description": "The street line ONLY — never the city or country. Sent to the courier network as its own field."
          },
          "city": {
            "type": "string"
          },
          "country": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2,
            "pattern": "^[A-Za-z]{2}$",
            "description": "ISO-3166-1 alpha-2, e.g. \"CL\". REQUIRED: without it the courier network resolves the address against its default market and can price the quote in the wrong currency. LastFolk never infers it."
          },
          "state": {
            "type": "string",
            "description": "Subdivision, where the country uses one. Optional."
          },
          "postal_code": {
            "type": "string",
            "description": "Optional."
          },
          "latitude": {
            "type": "number",
            "minimum": -90,
            "maximum": 90
          },
          "longitude": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          },
          "details": {
            "type": "string",
            "description": "Apartment, floor, access notes."
          }
        }
      },
      "Quote": {
        "type": "object",
        "required": [
          "quote_id",
          "expires_at",
          "price",
          "eta",
          "request_id"
        ],
        "properties": {
          "quote_id": {
            "type": "string",
            "format": "uuid",
            "description": "Single-use. Dispatch it with POST /v1/deliveries before expires_at."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "price": {
            "type": "object",
            "required": [
              "amount",
              "currency"
            ],
            "properties": {
              "amount": {
                "type": "string",
                "description": "Decimal string in `currency`, e.g. \"12.5\" or \"3500\". Never a float; parse decimal-safe."
              },
              "currency": {
                "type": "string",
                "description": "ISO 4217, e.g. USD, CLP, EUR."
              }
            }
          },
          "eta": {
            "type": "object",
            "required": [
              "pickup_at",
              "dropoff_at",
              "duration_minutes"
            ],
            "properties": {
              "pickup_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "dropoff_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "duration_minutes": {
                "type": [
                  "number",
                  "null"
                ]
              }
            }
          },
          "request_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "DeliveryStatus": {
        "type": "string",
        "description": "The delivery lifecycle vocabulary. ADDITIVE: new statuses may be added; treat unknown values as in-progress.",
        "enum": [
          "pending",
          "dispatched",
          "pickup",
          "in_transit",
          "delivered",
          "cancelled",
          "failed",
          "returned"
        ]
      },
      "Delivery": {
        "type": "object",
        "required": [
          "delivery_id",
          "status",
          "tracking_url",
          "quote_id",
          "external_order_id",
          "request_id"
        ],
        "properties": {
          "delivery_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "$ref": "#/components/schemas/DeliveryStatus"
          },
          "tracking_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "May be null for a moment right after creation."
          },
          "quote_id": {
            "type": "string",
            "format": "uuid"
          },
          "external_order_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "request_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "DeliveryDetail": {
        "type": "object",
        "required": [
          "delivery_id",
          "status",
          "tracking_url",
          "created_at",
          "updated_at",
          "status_updated_at",
          "pickup_eta",
          "dropoff_eta",
          "external_order_id",
          "quote_id",
          "request_id",
          "verification_pin"
        ],
        "properties": {
          "delivery_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "$ref": "#/components/schemas/DeliveryStatus"
          },
          "tracking_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "status_updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "pickup_eta": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "dropoff_eta": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "external_order_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "quote_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "request_id": {
            "type": "string",
            "format": "uuid"
          },
          "verification_pin": {
            "type": [
              "string",
              "null"
            ],
            "description": "Dropoff verification PIN the recipient gives the courier. Null until the provider issues it, and on deliveries that carry none. Deliberately NOT included in the delivery.status_changed webhook payload — read it here, over an authenticated request."
          }
        }
      },
      "BillingProfile": {
        "type": "object",
        "description": "Exactly these seven fields; each is a string or null.",
        "required": [
          "legal_company_name",
          "tax_id",
          "tax_id_type",
          "billing_address_street",
          "billing_address_city",
          "billing_address_postal_code",
          "billing_address_country"
        ],
        "properties": {
          "legal_company_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "tax_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "tax_id_type": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "VAT",
              "RUT",
              "RFC",
              "EIN",
              "CUIT",
              "NIT",
              "OTHER",
              null
            ]
          },
          "billing_address_street": {
            "type": [
              "string",
              "null"
            ]
          },
          "billing_address_city": {
            "type": [
              "string",
              "null"
            ]
          },
          "billing_address_postal_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "billing_address_country": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 3166-1 alpha-2, uppercased on write."
          }
        }
      },
      "BillingProfileUpdate": {
        "type": "object",
        "description": "Any subset of the seven BillingProfile fields. Absent/empty = keep stored value; JSON null = clear.",
        "properties": {
          "legal_company_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "tax_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "tax_id_type": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "VAT",
              "RUT",
              "RFC",
              "EIN",
              "CUIT",
              "NIT",
              "OTHER",
              null
            ]
          },
          "billing_address_street": {
            "type": [
              "string",
              "null"
            ]
          },
          "billing_address_city": {
            "type": [
              "string",
              "null"
            ]
          },
          "billing_address_postal_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "billing_address_country": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "description": "The body of every outgoing webhook POST. Signed with X-LastFolk-Signature (t=<unix>,v1=HMAC-SHA256(secret, `${t}.${rawBody}`)) over this exact raw body; X-LastFolk-Event-Id carries `id` for deduplication. v1 has one event type: delivery.status_changed.",
        "required": [
          "id",
          "type",
          "created_at",
          "environment",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "type": {
            "type": "string",
            "enum": [
              "delivery.status_changed"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "environment": {
            "$ref": "#/components/schemas/Environment"
          },
          "data": {
            "type": "object",
            "required": [
              "delivery_id",
              "status",
              "previous_status",
              "tracking_url",
              "external_order_id",
              "quote_id",
              "occurred_at"
            ],
            "properties": {
              "delivery_id": {
                "type": "string",
                "format": "uuid"
              },
              "status": {
                "$ref": "#/components/schemas/DeliveryStatus"
              },
              "previous_status": {
                "$ref": "#/components/schemas/DeliveryStatus"
              },
              "tracking_url": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "external_order_id": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "quote_id": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uuid"
              },
              "occurred_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "DeliveryListItem": {
        "type": "object",
        "description": "One delivery in a list. Identical to DeliveryDetail EXCEPT that it carries no `verification_pin`: the dropoff PIN is a shared secret and is served only by GET /v1/deliveries/{delivery_id}, one delivery at a time.",
        "required": [
          "delivery_id",
          "status",
          "tracking_url",
          "created_at",
          "updated_at",
          "status_updated_at",
          "pickup_eta",
          "dropoff_eta",
          "external_order_id",
          "quote_id",
          "request_id"
        ],
        "properties": {
          "delivery_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "$ref": "#/components/schemas/DeliveryStatus"
          },
          "tracking_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "status_updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "pickup_eta": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "dropoff_eta": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "external_order_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "quote_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "request_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "DeliveryList": {
        "type": "object",
        "required": [
          "data",
          "has_more",
          "next_cursor",
          "request_id"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DeliveryListItem"
            }
          },
          "has_more": {
            "type": "boolean",
            "description": "True when another page exists."
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` to fetch the next page. OPAQUE — its contents are an implementation detail and may change without notice. Do not parse it, construct one, or store it as a durable identifier; treat it as a token you received and hand back."
          },
          "request_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "ManifestDimensions": {
        "type": "object",
        "description": "Package dimensions in CENTIMETERS, in the courier network's own keys. All three are required together.",
        "required": [
          "length",
          "height",
          "depth"
        ],
        "properties": {
          "length": {
            "type": "number",
            "exclusiveMinimum": 0
          },
          "height": {
            "type": "number",
            "exclusiveMinimum": 0
          },
          "depth": {
            "type": "number",
            "exclusiveMinimum": 0
          }
        }
      },
      "ManifestItem": {
        "type": "object",
        "description": "One declared line of the shipment. Declare what you measure — weight in grams, dimensions in cm, money in fixed x100 subunits of the order currency (no exception for zero-decimal currencies: CLP $12.500 is 1250000). Do NOT send a size: the courier network derives it from weight and dimensions, and a request carrying manifest.items[].size is refused as invalid_request. Every field is optional, but a line must declare at least one of name, weight, dimensions or price.",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "quantity": {
            "type": "integer",
            "minimum": 1
          },
          "weight": {
            "type": "integer",
            "minimum": 1,
            "description": "Per-unit weight in GRAMS."
          },
          "dimensions": {
            "$ref": "#/components/schemas/ManifestDimensions"
          },
          "price": {
            "type": "integer",
            "minimum": 0,
            "description": "Per-unit price, fixed x100 subunits of the order currency."
          },
          "must_be_upright": {
            "type": "boolean"
          }
        }
      },
      "DeclaredManifest": {
        "type": "object",
        "description": "What ships, declared by you and forwarded to the courier network verbatim — LastFolk computes nothing from it. At most 45 lines (a LastFolk cap under the provider's observed limits, not a documented courier-network number); aggregate lines rather than splitting per unit. When you omit the manifest entirely nothing is fabricated in its place, and the omission is recorded on our side.",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 45,
            "items": {
              "$ref": "#/components/schemas/ManifestItem"
            }
          },
          "total_value": {
            "type": "integer",
            "minimum": 0,
            "description": "Declared goods value, fixed x100 subunits of the order currency."
          }
        }
      }
    }
  }
}
