{
  "openapi": "3.1.0",
  "info": {
    "title": "GCash Scan User API",
    "version": "1.1.0",
    "description": "Public API for balance queries, idempotent QR order submission, asynchronous GCash extraction tasks, local recovery by client_request_id, and signed callbacks. Existing v1 requests remain compatible. Monetary values are decimal strings with exactly two fractional digits and use unit U (USDT).",
    "license": {
      "name": "Proprietary",
      "identifier": "LicenseRef-Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://scan-api.fengl.cc/api/public/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    },
    {
      "BearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "Balance and current fee discovery."
    },
    {
      "name": "Orders",
      "description": "Idempotent QR scan orders and recovery queries."
    },
    {
      "name": "Extractions",
      "description": "Idempotent GCash extraction tasks and recovery queries."
    }
  ],
  "paths": {
    "/balance": {
      "get": {
        "operationId": "getBalance",
        "summary": "Get balances and current service fees",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "Current balances and fees",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Balance"
                },
                "example": {
                  "available": "25.50",
                  "locked": "1.00",
                  "unit": "U",
                  "extraction_fee": "1.00",
                  "order_fee": "1.00"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/orders": {
      "post": {
        "operationId": "createOrder",
        "summary": "Upload a QR image and create an order",
        "tags": [
          "Orders"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "qr"
                ],
                "properties": {
                  "qr": {
                    "type": "string",
                    "format": "binary",
                    "description": "PNG, JPEG, WebP, or GIF image, maximum 10 MiB."
                  },
                  "client_request_id": {
                    "type": "string",
                    "maxLength": 128,
                    "pattern": "^[A-Za-z0-9][A-Za-z0-9._:@+=-]{0,127}$"
                  },
                  "task_no": {
                    "$ref": "#/components/schemas/Identifier"
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 320
                  },
                  "callback_url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048
                  },
                  "callback_secret": {
                    "type": "string",
                    "maxLength": 512,
                    "writeOnly": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Idempotent replay; existing order returned and no fee charged",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderCreated"
                },
                "example": {
                  "order_no": "01K20A1BCDEF2GH3JK4MNP5QRS",
                  "task_no": "01K20B2CDEFG3HJ4KM5NP6QRST",
                  "client_request_id": "local-batch-email-order-001",
                  "status": "QUEUED",
                  "stage": "queued",
                  "fee": "1.00",
                  "unit": "U",
                  "error": null,
                  "message": null,
                  "email": "user@example.com",
                  "queued_at": "2026-08-07T08:20:00Z",
                  "claimed_at": null,
                  "completed_at": null,
                  "expires_at": "2026-08-07T08:30:00Z",
                  "idempotent_replay": true
                }
              }
            }
          },
          "201": {
            "description": "Order created and fee reserved",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderCreated"
                },
                "example": {
                  "order_no": "01K20A1BCDEF2GH3JK4MNP5QRS",
                  "task_no": "01K20B2CDEFG3HJ4KM5NP6QRST",
                  "client_request_id": "local-batch-email-order-001",
                  "status": "QUEUED",
                  "stage": "queued",
                  "fee": "1.00",
                  "unit": "U",
                  "error": null,
                  "message": null,
                  "email": "user@example.com",
                  "queued_at": "2026-08-07T08:20:00Z",
                  "claimed_at": null,
                  "completed_at": null,
                  "expires_at": "2026-08-07T08:30:00Z",
                  "idempotent_replay": false
                }
              }
            }
          },
          "400": {
            "description": "Invalid multipart fields or QR image",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "409": {
            "description": "Idempotency conflict, expired QR, or extraction already linked",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "idempotency_conflict",
                  "message": "The idempotency key was already used with different content"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported image type",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "unsupported_image",
                  "message": "The uploaded image format is not supported"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "QR storage unavailable",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Existing clients may send only qr. When client_request_id is supplied, the same key and identical raw QR bytes return the existing order with HTTP 200 and no second charge; different bytes return HTTP 409 idempotency_conflict. task_no links the order to an extraction task owned by the same user. Callback settings inherit from the linked extraction unless explicitly supplied."
      }
    },
    "/orders/{order_no}": {
      "get": {
        "operationId": "getOrder",
        "summary": "Get an order owned by the authenticated user",
        "tags": [
          "Orders"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/OrderNo"
          }
        ],
        "responses": {
          "200": {
            "description": "Order details",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                },
                "example": {
                  "order_no": "01K20A1BCDEF2GH3JK4MNP5QRS",
                  "task_no": "01K20B2CDEFG3HJ4KM5NP6QRST",
                  "client_request_id": "local-batch-email-order-001",
                  "status": "COMPLETED",
                  "stage": "scan_completed",
                  "fee": "1.00",
                  "unit": "U",
                  "error": null,
                  "message": null,
                  "email": "user@example.com",
                  "queued_at": "2026-08-07T08:20:00Z",
                  "claimed_at": null,
                  "completed_at": "2026-08-07T08:22:00Z",
                  "expires_at": "2026-08-07T08:30:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/extractions": {
      "post": {
        "operationId": "createExtraction",
        "summary": "Create an asynchronous GCash extraction task",
        "tags": [
          "Extractions"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExtractionRequest"
              },
              "example": {
                "access_token": "FULL_ACCESS_TOKEN_AT_LEAST_20_CHARS",
                "client_request_id": "local-batch-email-hash-001",
                "email": "user@example.com",
                "metadata": {
                  "batch_id": "20260807_001",
                  "channel": "gc-plus"
                },
                "callback_url": "https://hooks.example.com/gc-plus/callback",
                "callback_secret": "optional-secret"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Idempotent replay; existing task returned and no fee charged",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExtractionAccepted"
                },
                "example": {
                  "task_no": "01K20B2CDEFG3HJ4KM5NP6QRST",
                  "client_request_id": "local-batch-email-hash-001",
                  "linked_order_no": null,
                  "status": "QUEUED",
                  "stage": "queued",
                  "fee": "1.00",
                  "unit": "U",
                  "long_url": null,
                  "copy_paste": null,
                  "image_url_png": null,
                  "error": null,
                  "message": null,
                  "email": "user@example.com",
                  "metadata": {
                    "batch_id": "20260807_001",
                    "channel": "gc-plus"
                  },
                  "created_at": "2026-08-07T08:20:00Z",
                  "started_at": "2026-08-07T08:20:01Z",
                  "finished_at": null,
                  "expires_at": "2026-08-07T08:30:00Z",
                  "idempotent_replay": true
                }
              }
            }
          },
          "202": {
            "description": "Task accepted and fee reserved",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExtractionAccepted"
                },
                "example": {
                  "task_no": "01K20B2CDEFG3HJ4KM5NP6QRST",
                  "client_request_id": "local-batch-email-hash-001",
                  "linked_order_no": null,
                  "status": "QUEUED",
                  "stage": "queued",
                  "fee": "1.00",
                  "unit": "U",
                  "long_url": null,
                  "copy_paste": null,
                  "image_url_png": null,
                  "error": null,
                  "message": null,
                  "email": "user@example.com",
                  "metadata": {
                    "batch_id": "20260807_001",
                    "channel": "gc-plus"
                  },
                  "created_at": "2026-08-07T08:20:00Z",
                  "started_at": "2026-08-07T08:20:01Z",
                  "finished_at": null,
                  "expires_at": "2026-08-07T08:30:00Z",
                  "idempotent_replay": false
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON, token, metadata, email, callback, or client_request_id",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientBalance"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "409": {
            "description": "The idempotency key was used with a different access token",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "idempotency_conflict",
                  "message": "The idempotency key was already used with different content"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "External submission failed and reserved funds were released",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExtractionSubmitFailed"
                }
              }
            }
          },
          "503": {
            "description": "Extraction service disabled or unavailable",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "extractor_unavailable",
                  "message": "The extraction service is unavailable"
                }
              }
            }
          }
        },
        "description": "Existing clients may send only access_token. When client_request_id is supplied, the same key and same trimmed access-token fingerprint return the existing task with HTTP 200 and no second charge; a different token returns HTTP 409 idempotency_conflict. The raw access token is never persisted."
      }
    },
    "/extractions/{task_no}": {
      "get": {
        "operationId": "getExtraction",
        "summary": "Get an extraction task owned by the authenticated user",
        "tags": [
          "Extractions"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TaskNo"
          }
        ],
        "responses": {
          "200": {
            "description": "Extraction details",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Extraction"
                },
                "example": {
                  "task_no": "01K20B2CDEFG3HJ4KM5NP6QRST",
                  "client_request_id": "local-batch-email-hash-001",
                  "linked_order_no": null,
                  "status": "SUCCESS",
                  "stage": "url_ready",
                  "fee": "1.00",
                  "unit": "U",
                  "long_url": "https://example.com/gcash/result",
                  "copy_paste": "https://example.com/gcash/result",
                  "image_url_png": null,
                  "error": null,
                  "message": null,
                  "email": "user@example.com",
                  "metadata": {
                    "batch_id": "20260807_001",
                    "channel": "gc-plus"
                  },
                  "created_at": "2026-08-07T08:20:00Z",
                  "started_at": "2026-08-07T08:20:01Z",
                  "finished_at": "2026-08-07T08:20:08Z",
                  "expires_at": "2026-08-07T08:30:00Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/orders/by-client-request/{client_request_id}": {
      "get": {
        "operationId": "getOrderByClientRequestId",
        "summary": "Recover an order by client_request_id",
        "tags": [
          "Orders"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ClientRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Order details",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/extractions/by-client-request/{client_request_id}": {
      "get": {
        "operationId": "getExtractionByClientRequestId",
        "summary": "Recover an extraction by client_request_id",
        "tags": [
          "Extractions"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ClientRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Extraction details",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Extraction"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Recommended. API key beginning with gcs_."
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "gcs_ API key",
        "description": "Alternative to X-API-Key. The Bearer prefix is case-sensitive."
      }
    },
    "headers": {
      "RequestId": {
        "description": "Request tracing identifier",
        "schema": {
          "type": "string"
        }
      },
      "RetryAfter": {
        "description": "Seconds before retrying",
        "schema": {
          "type": "integer",
          "const": 60
        }
      }
    },
    "parameters": {
      "OrderNo": {
        "name": "order_no",
        "in": "path",
        "required": true,
        "description": "Order identifier returned by createOrder",
        "schema": {
          "$ref": "#/components/schemas/Identifier"
        }
      },
      "TaskNo": {
        "name": "task_no",
        "in": "path",
        "required": true,
        "description": "Extraction task identifier returned by createExtraction",
        "schema": {
          "$ref": "#/components/schemas/Identifier"
        }
      },
      "ClientRequestId": {
        "name": "client_request_id",
        "in": "path",
        "required": true,
        "description": "The exact client_request_id supplied during creation.",
        "schema": {
          "type": "string",
          "maxLength": 128,
          "pattern": "^[A-Za-z0-9][A-Za-z0-9._:@+=-]{0,127}$"
        }
      }
    },
    "schemas": {
      "Identifier": {
        "type": "string",
        "minLength": 26,
        "maxLength": 26,
        "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
      },
      "Money": {
        "type": "string",
        "pattern": "^(0|[1-9][0-9]*)\\.[0-9]{2}$",
        "examples": [
          "0.50",
          "1.00"
        ]
      },
      "Unit": {
        "type": "string",
        "const": "U"
      },
      "OrderStatus": {
        "type": "string",
        "enum": [
          "QUEUED",
          "CLAIMED",
          "COMPLETED",
          "CANCELLED",
          "EXPIRED",
          "REFUNDED"
        ]
      },
      "ExtractionStatus": {
        "type": "string",
        "enum": [
          "CREATED",
          "QUEUED",
          "RUNNING",
          "SUCCESS",
          "FAILED",
          "EXPIRED"
        ]
      },
      "Balance": {
        "type": "object",
        "required": [
          "available",
          "locked",
          "unit",
          "extraction_fee",
          "order_fee"
        ],
        "properties": {
          "available": {
            "$ref": "#/components/schemas/Money"
          },
          "locked": {
            "$ref": "#/components/schemas/Money"
          },
          "unit": {
            "$ref": "#/components/schemas/Unit"
          },
          "extraction_fee": {
            "$ref": "#/components/schemas/Money"
          },
          "order_fee": {
            "$ref": "#/components/schemas/Money"
          },
          "channels": {
            "type": "array",
            "description": "Current fees by link_type.",
            "items": {
              "$ref": "#/components/schemas/ChannelPrice"
            }
          }
        }
      },
      "ChannelPrice": {
        "type": "object",
        "required": ["link_type", "link_type_label", "scan_fee", "extract_fee"],
        "properties": {
          "link_type": {"type": "string"},
          "link_type_label": {"type": "string"},
          "scan_fee": {"$ref": "#/components/schemas/Money"},
          "extract_fee": {"$ref": "#/components/schemas/Money"}
        }
      },
      "OrderCreated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Order"
          },
          {
            "type": "object",
            "required": [
              "idempotent_replay"
            ],
            "properties": {
              "idempotent_replay": {
                "type": "boolean"
              }
            }
          }
        ]
      },
      "Order": {
        "type": "object",
        "required": [
          "order_no",
          "task_no",
          "client_request_id",
          "status",
          "stage",
          "fee",
          "unit",
          "error",
          "message",
          "email",
          "queued_at",
          "claimed_at",
          "completed_at",
          "expires_at"
        ],
        "properties": {
          "order_no": {
            "$ref": "#/components/schemas/Identifier"
          },
          "task_no": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Identifier"
              },
              {
                "type": "null"
              }
            ]
          },
          "client_request_id": {
            "$ref": "#/components/schemas/ClientRequestId"
          },
          "status": {
            "$ref": "#/components/schemas/OrderStatus"
          },
          "stage": {
            "$ref": "#/components/schemas/OrderStage"
          },
          "fee": {
            "$ref": "#/components/schemas/Money"
          },
          "unit": {
            "$ref": "#/components/schemas/Unit"
          },
          "error": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ErrorCode"
              },
              {
                "type": "null"
              }
            ]
          },
          "message": {
            "$ref": "#/components/schemas/NullableString"
          },
          "email": {
            "$ref": "#/components/schemas/NullableString"
          },
          "queued_at": {
            "type": "string",
            "format": "date-time"
          },
          "claimed_at": {
            "$ref": "#/components/schemas/NullableTimestamp"
          },
          "completed_at": {
            "$ref": "#/components/schemas/NullableTimestamp"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ExtractionRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/AutomationFields"
          },
          {
            "type": "object",
            "required": [
              "access_token"
            ],
            "properties": {
              "access_token": {
                "type": "string",
                "minLength": 20,
                "maxLength": 8192,
                "writeOnly": true,
                "description": "Forwarded to the extractor and never stored or logged. Only its SHA-256 fingerprint is retained for idempotency."
              },
              "metadata": {
                "type": "object",
                "maxProperties": 256,
                "additionalProperties": true,
                "description": "Optional JSON object, encoded size at most 16 KiB."
              }
            }
          }
        ]
      },
      "ExtractionAccepted": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Extraction"
          },
          {
            "type": "object",
            "required": [
              "idempotent_replay"
            ],
            "properties": {
              "idempotent_replay": {
                "type": "boolean"
              }
            }
          }
        ]
      },
      "Extraction": {
        "type": "object",
        "required": [
          "task_no",
          "client_request_id",
          "linked_order_no",
          "status",
          "stage",
          "fee",
          "unit",
          "long_url",
          "copy_paste",
          "image_url_png",
          "error",
          "message",
          "email",
          "metadata",
          "created_at",
          "started_at",
          "finished_at",
          "expires_at"
        ],
        "properties": {
          "task_no": {
            "$ref": "#/components/schemas/Identifier"
          },
          "client_request_id": {
            "$ref": "#/components/schemas/ClientRequestId"
          },
          "linked_order_no": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Identifier"
              },
              {
                "type": "null"
              }
            ]
          },
          "status": {
            "$ref": "#/components/schemas/ExtractionStatus"
          },
          "stage": {
            "$ref": "#/components/schemas/ExtractionStage"
          },
          "fee": {
            "$ref": "#/components/schemas/Money"
          },
          "unit": {
            "$ref": "#/components/schemas/Unit"
          },
          "long_url": {
            "$ref": "#/components/schemas/NullableString"
          },
          "copy_paste": {
            "$ref": "#/components/schemas/NullableString"
          },
          "image_url_png": {
            "$ref": "#/components/schemas/NullableString"
          },
          "error": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ErrorCode"
              },
              {
                "type": "null"
              }
            ]
          },
          "message": {
            "$ref": "#/components/schemas/NullableString"
          },
          "email": {
            "$ref": "#/components/schemas/NullableString"
          },
          "metadata": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "started_at": {
            "$ref": "#/components/schemas/NullableTimestamp"
          },
          "finished_at": {
            "$ref": "#/components/schemas/NullableTimestamp"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "message": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "InsufficientBalance": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "error",
          "required",
          "available",
          "unit"
        ],
        "properties": {
          "error": {
            "type": "string",
            "const": "insufficient_balance"
          },
          "required": {
            "$ref": "#/components/schemas/Money"
          },
          "available": {
            "$ref": "#/components/schemas/Money"
          },
          "unit": {
            "$ref": "#/components/schemas/Unit"
          }
        }
      },
      "ExtractionSubmitFailed": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "error",
          "task_no",
          "status",
          "released"
        ],
        "properties": {
          "error": {
            "type": "string",
            "const": "extraction_submit_failed"
          },
          "task_no": {
            "$ref": "#/components/schemas/Identifier"
          },
          "status": {
            "type": "string",
            "const": "FAILED"
          },
          "released": {
            "type": "boolean",
            "const": true
          }
        }
      },
      "ClientRequestId": {
        "type": [
          "string",
          "null"
        ],
        "maxLength": 128,
        "pattern": "^[A-Za-z0-9][A-Za-z0-9._:@+=-]{0,127}$",
        "examples": [
          "local-batch-email-hash-001"
        ]
      },
      "NullableString": {
        "type": [
          "string",
          "null"
        ]
      },
      "NullableTimestamp": {
        "type": [
          "string",
          "null"
        ],
        "format": "date-time"
      },
      "ExtractionStage": {
        "type": "string",
        "enum": [
          "created",
          "queued",
          "running",
          "url_ready",
          "failed",
          "expired"
        ]
      },
      "OrderStage": {
        "type": "string",
        "enum": [
          "queued",
          "claimed",
          "waiting_scan",
          "scan_completed",
          "scan_failed",
          "expired",
          "cancelled",
          "refunded"
        ]
      },
      "ErrorCode": {
        "type": "string",
        "enum": [
          "invalid_api_key",
          "ip_not_allowed",
          "insufficient_balance",
          "invalid_access_token",
          "extraction_submit_failed",
          "extractor_unavailable",
          "no_url_returned",
          "extraction_failed",
          "extraction_timeout",
          "qr_required",
          "invalid_qr",
          "qr_expired",
          "unsupported_image",
          "order_timeout",
          "scan_failed",
          "worker_cancelled",
          "idempotency_conflict",
          "rate_limit_exceeded",
          "internal_error",
          "invalid_request",
          "invalid_client_request_id",
          "invalid_email",
          "invalid_metadata",
          "invalid_callback_url",
          "invalid_callback_secret",
          "task_not_found",
          "task_already_linked",
          "not_found",
          "user_disabled"
        ]
      },
      "AutomationFields": {
        "type": "object",
        "properties": {
          "client_request_id": {
            "type": "string",
            "maxLength": 128,
            "pattern": "^[A-Za-z0-9][A-Za-z0-9._:@+=-]{0,127}$",
            "description": "Idempotency and recovery key scoped to the authenticated API user."
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 320
          },
          "callback_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Absolute HTTP or HTTPS callback URL whose host resolves to a public IP. Loopback, private, and link-local destinations are rejected. Redirects are not followed."
          },
          "callback_secret": {
            "type": "string",
            "maxLength": 512,
            "writeOnly": true,
            "description": "Optional HMAC secret. Requires callback_url and is encrypted at rest."
          }
        }
      },
      "ExtractionCallback": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Extraction"
          },
          {
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "const": "extraction.updated"
              }
            }
          }
        ]
      },
      "OrderCallback": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Order"
          },
          {
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "const": "order.updated"
              }
            }
          }
        ]
      }
    },
    "responses": {
      "InvalidApiKey": {
        "description": "API key is missing, invalid, reset, disabled, or belongs to a disabled user",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "invalid_api_key",
              "message": "invalid_api_key"
            }
          }
        }
      },
      "IpNotAllowed": {
        "description": "Source IP is not on the configured whitelist",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "ip_not_allowed",
              "ip": "203.0.113.10",
              "message": "ip_not_allowed"
            }
          }
        }
      },
      "InsufficientBalance": {
        "description": "Available balance is lower than the current service fee",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/InsufficientBalance"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource does not exist or is not owned by the authenticated user",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "not_found",
              "message": "not_found"
            }
          }
        }
      },
      "RateLimited": {
        "description": "More than 120 requests were made for this API key in the current UTC minute",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "rate_limit_exceeded",
              "message": "rate_limit_exceeded"
            }
          }
        }
      },
      "InternalError": {
        "description": "Unexpected internal or database error",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "internal_error",
              "message": "internal_error"
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "extractionUpdated": {
      "post": {
        "operationId": "receiveExtractionUpdated",
        "summary": "Extraction state changed",
        "description": "A 2xx response acknowledges delivery. Non-2xx and network failures are retried five times after the initial attempt with increasing delays.",
        "parameters": [
          {
            "name": "X-Request-ID",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Stable for every retry of one event."
          },
          {
            "name": "X-Signature",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            },
            "description": "Lowercase hex HMAC-SHA256 of the exact raw JSON body when callback_secret was supplied."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExtractionCallback"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Callback accepted"
          }
        }
      }
    },
    "orderUpdated": {
      "post": {
        "operationId": "receiveOrderUpdated",
        "summary": "Order state changed",
        "description": "A 2xx response acknowledges delivery. Non-2xx and network failures are retried five times after the initial attempt with increasing delays.",
        "parameters": [
          {
            "name": "X-Request-ID",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Stable for every retry of one event."
          },
          {
            "name": "X-Signature",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            },
            "description": "Lowercase hex HMAC-SHA256 of the exact raw JSON body when callback_secret was supplied."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrderCallback"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Callback accepted"
          }
        }
      }
    }
  }
}
