> ## Documentation Index
> Fetch the complete documentation index at: https://docs.moduluslabs.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload Onboarding Files

> Upload business documents for merchant onboarding verification

## Overview

Uploads business documents required for merchant onboarding and KYC (Know Your Customer) verification. This endpoint accepts multiple document types including valid IDs, business permits, certificates, and supporting documents.

<Warning>
  **Binary File Upload Required:** This endpoint expects actual binary file content via `multipart/form-data`. Do NOT send base64-encoded strings, file paths, or URLs. Upload the actual file data using proper multipart form encoding.
</Warning>

<Warning>
  **Rate Limiting:** Maximum 5 requests per 60 seconds to prevent abuse.
</Warning>

<Info>
  **Security:** This endpoint implements OWASP File Upload Cheat Sheet best practices including file extension validation, MIME type validation, content validation using magic bytes, file size limits, and secure storage in S3 with UUID naming.
</Info>

## Authentication

This endpoint requires JWT Bearer Token authentication.

```bash theme={null}
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

## Authorization Rules

| User Role             | Allowed Actions                                                                        |
| --------------------- | -------------------------------------------------------------------------------------- |
| Regular User/Merchant | Can only upload files for their own account (merchantId must match authenticated user) |
| Super Admin           | Can upload files on behalf of any merchant                                             |

## Path Parameters

<ParamField path="merchantId" type="string" required>
  The merchant's account ID

  **Format:** Numeric string (digits only)

  **Pattern:** `^\d+$`

  **Example:** `"12345"`

  <Warning>
    Must match the authenticated user's account ID unless the user is a Super Admin.
  </Warning>
</ParamField>

## Request Body

<Note>
  **Content-Type:** This endpoint requires `multipart/form-data` encoding. Each field should contain the actual binary file content, not strings, URLs, or base64-encoded data.
</Note>

This endpoint accepts binary file uploads via `multipart/form-data`. When uploading files, ensure you're using proper file upload mechanisms in your programming language (e.g., `fs.createReadStream()` in Node.js, `open()` in Python, `CURLFile` in PHP) rather than sending text or encoded strings.

### File Requirements

<CardGroup cols={2}>
  <Card title="Allowed Formats" icon="file-image">
    JPEG, JPG, PNG, PDF
  </Card>

  <Card title="File Size Limit" icon="weight-scale">
    25MB per file maximum
  </Card>

  <Card title="File Name Length" icon="text-width">
    255 characters maximum
  </Card>

  <Card title="Validation Levels" icon="shield-check">
    Extension, MIME type, and magic bytes
  </Card>
</CardGroup>

### Required Documents by Business Type

<AccordionGroup>
  <Accordion title="STARTER" icon="store">
    **Required Documents (6 total):**

    * Valid ID(s) of Signatory
    * Photo of store with store name
    * Barangay Business Permit
    * Incorporator's Government Issued ID
    * Signatory's Government Issued ID
    * Authorized Representative Government Issued ID

    **Purpose:** For small businesses and micro-enterprises starting their operations.
  </Accordion>

  <Accordion title="SOLE_PROPRIETOR" icon="user">
    **Required Documents (5 total):**

    * Valid ID(s) of Signatory
    * Incorporator's Government Issued ID
    * Signatory's Government Issued ID
    * Authorized Representative Government Issued ID
    * **Either of the two:**
      * Business/Mayor's Permit **OR**
      * BIR Certificate of Registration (2303)

    **Purpose:** For businesses owned and operated by a single individual.
  </Accordion>

  <Accordion title="PARTNERSHIP" icon="handshake">
    **Required Documents (9 total):**

    * Valid ID(s) of Signatory
    * Incorporator's Government Issued ID
    * Signatory's Government Issued ID
    * Authorized Representative Government Issued ID
    * List of Goods/Services sold with Pricing
    * Refund and Refund Policy
    * Fulfillment Policy
    * Privacy Policy
    * **At least one of the following:**
      * Business/Mayor's Permit **OR**
      * BIR Certificate of Registration (2303) **OR**
      * SEC Registration Certificate

    **Optional Documents:**

    * AOI and By-Laws
    * Secretary's Certificate

    **Purpose:** For businesses owned by two or more partners.
  </Accordion>

  <Accordion title="CORPORATION" icon="building">
    **Required Documents (10 total):**

    * Valid ID(s) of Signatory
    * Incorporator's Government Issued ID
    * Signatory's Government Issued ID
    * Authorized Representative Government Issued ID
    * List of Goods/Services sold with Pricing
    * Refund and Refund Policy
    * Fulfillment Policy
    * Privacy Policy
    * **At least one of the following:**
      * Mayor's or Business Permit **OR**
      * BIR Certificate 2303 **OR**
      * SEC Certificate
    * **Either of the two:**
      * Article of Partnership/Incorporation (AOI) and By-Laws **OR**
      * General Information Sheet (GIS)
    * Secretary's Certificate (mandatory for Corporation)

    **Purpose:** For registered corporations and incorporated entities.
  </Accordion>
</AccordionGroup>

### Document Fields

Each document type supports front, back, and signature images where applicable. All fields accept arrays to support multiple file uploads.

#### Valid IDs

<ParamField body="idsOfValidSignatories" type="file">
  Valid government-issued IDs of authorized signatories (front side)

  **Required for:** All business types

  **Type:** Binary file upload (not string)

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file

  **Examples:** Driver's License, Passport, National ID, SSS ID, TIN ID

  <Warning>
    Upload the actual image/PDF file, not a file path, URL, or base64 string
  </Warning>
</ParamField>

<ParamField body="idsOfValidSignatoriesBack" type="file">
  Back side of valid IDs

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

<ParamField body="idsOfValidSignatoriesSignature" type="file">
  Signature specimen from valid IDs

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

#### Mayor's Permit / Business Permit

<ParamField body="mayorsOrBusinessPermit" type="file">
  Mayor's Permit or Business Permit (front)

  **Required for:** SOLE\_PROPRIETOR, PARTNERSHIP, CORPORATION (one of three options)

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

<ParamField body="mayorsOrBusinessPermitBack" type="file">
  Back side of Mayor's/Business Permit

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

<ParamField body="mayorsOrBusinessPermitSignature" type="file">
  Signature page of Mayor's/Business Permit

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

#### BIR Certificate 2303

<ParamField body="birCert2303" type="file">
  BIR Certificate of Registration (Form 2303) - front

  **Required for:** SOLE\_PROPRIETOR (alternative), PARTNERSHIP/CORPORATION (one of three options)

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

<ParamField body="birCert2303Back" type="file">
  Back side of BIR Certificate 2303

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

<ParamField body="birCert2303Signature" type="file">
  Signature page of BIR Certificate 2303

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

#### SEC Certificate

<ParamField body="secCert" type="file">
  SEC (Securities and Exchange Commission) Certificate - front

  **Required for:** PARTNERSHIP, CORPORATION (one of three options)

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

<ParamField body="secCertBack" type="file">
  Back side of SEC Certificate

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

<ParamField body="secCertSignature" type="file">
  Signature page of SEC Certificate

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

#### General Information Sheet (GIS)

<ParamField body="gis" type="file">
  General Information Sheet - front

  **Required for:** CORPORATION (alternative to AOI/By-Laws)

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

<ParamField body="gisBack" type="file">
  Back side of GIS

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

<ParamField body="gisSignature" type="file">
  Signature page of GIS

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

#### Articles of Incorporation and By-Laws

<ParamField body="aoiAndByLaws" type="file">
  Articles of Incorporation and By-Laws - front

  **Required for:** CORPORATION (alternative to GIS)

  **Optional for:** PARTNERSHIP

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

<ParamField body="aoiAndByLawsBack" type="file">
  Back side of AOI and By-Laws

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

<ParamField body="aoiAndByLawsSignature" type="file">
  Signature page of AOI and By-Laws

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

#### Secretary's Certificate

<ParamField body="secretarysCert" type="file">
  Secretary's Certificate - front

  **Required for:** CORPORATION

  **Optional for:** PARTNERSHIP

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

<ParamField body="secretarysCertBack" type="file">
  Back side of Secretary's Certificate

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

<ParamField body="secretarysCertSignature" type="file">
  Signature page of Secretary's Certificate

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

#### Barangay Business Permit

<ParamField body="barangayBusinessPermit" type="file">
  Barangay Business Permit - front

  **Required for:** STARTER business type

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

<ParamField body="barangayBusinessPermitBack" type="file">
  Back side of Barangay Business Permit

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

<ParamField body="barangayBusinessPermitSignature" type="file">
  Signature page of Barangay Business Permit

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG, PDF

  **Max size:** 25MB per file
</ParamField>

#### Store Photo

<ParamField body="storePhoto" type="file">
  Photo of the store/business establishment

  **Required for:** STARTER business type

  **Requirements:** Must show visible store name/signage

  **Type:** Binary file upload

  **Accepted formats:** JPEG, JPG, PNG

  **Max size:** 25MB per file
</ParamField>

<RequestExample>
  ```bash cURL theme={null}
  BEARER_TOKEN="your_jwt_token_here"
  MERCHANT_ID="12345"

  curl -X POST "https://kyc.sbx.moduluslabs.io/v2/onboard/${MERCHANT_ID}/files" \
    -H "Authorization: Bearer ${BEARER_TOKEN}" \
    -F "idsOfValidSignatories=@/path/to/valid_id_front.jpg" \
    -F "idsOfValidSignatoriesBack=@/path/to/valid_id_back.jpg" \
    -F "mayorsOrBusinessPermit=@/path/to/business_permit.pdf" \
    -F "storePhoto=@/path/to/store_photo.jpg"
  ```

  ```javascript Node.js theme={null}
  const axios = require('axios');
  const FormData = require('form-data');
  const fs = require('fs');

  const BEARER_TOKEN = process.env.BEARER_TOKEN;
  const MERCHANT_ID = '12345';

  async function uploadOnboardingFiles() {
    try {
      const form = new FormData();

      // Add files to form data
      form.append('idsOfValidSignatories',
        fs.createReadStream('/path/to/valid_id_front.jpg'));
      form.append('idsOfValidSignatoriesBack',
        fs.createReadStream('/path/to/valid_id_back.jpg'));
      form.append('mayorsOrBusinessPermit',
        fs.createReadStream('/path/to/business_permit.pdf'));
      form.append('storePhoto',
        fs.createReadStream('/path/to/store_photo.jpg'));

      const response = await axios.post(
        `https://kyc.sbx.moduluslabs.io/v2/onboard/${MERCHANT_ID}/files`,
        form,
        {
          headers: {
            'Authorization': `Bearer ${BEARER_TOKEN}`,
            ...form.getHeaders()
          },
          maxContentLength: Infinity,
          maxBodyLength: Infinity
        }
      );

      console.log(' Files uploaded successfully!');
      console.log(`Uploaded ${response.data.files.length} file(s)`);

      response.data.files.forEach(file => {
        console.log(`  • ${file.fileType}: ${file.name} (ID: ${file.id})`);
      });
    } catch (error) {
      if (error.response) {
        console.error(' Upload failed:', error.response.data.message);
        if (error.response.data.details) {
          console.error('Details:', error.response.data.details);
        }
      } else {
        console.error(' Error:', error.message);
      }
    }
  }

  uploadOnboardingFiles();
  ```

  ```python Python theme={null}
  import os
  import requests

  BEARER_TOKEN = os.getenv('BEARER_TOKEN')
  MERCHANT_ID = '12345'

  def upload_onboarding_files():
      url = f'https://kyc.sbx.moduluslabs.io/v2/onboard/{MERCHANT_ID}/files'

      headers = {
          'Authorization': f'Bearer {BEARER_TOKEN}'
      }

      # Prepare files for upload
      files = {
          'idsOfValidSignatories': open('/path/to/valid_id_front.jpg', 'rb'),
          'idsOfValidSignatoriesBack': open('/path/to/valid_id_back.jpg', 'rb'),
          'mayorsOrBusinessPermit': open('/path/to/business_permit.pdf', 'rb'),
          'storePhoto': open('/path/to/store_photo.jpg', 'rb')
      }

      try:
          response = requests.post(url, headers=headers, files=files)
          response.raise_for_status()

          data = response.json()
          print(' Files uploaded successfully!')
          print(f"Uploaded {len(data['files'])} file(s)")

          for file in data['files']:
              print(f"  • {file['fileType']}: {file.get('name', 'N/A')} (ID: {file['id']})")

      except requests.exceptions.HTTPError as error:
          error_data = error.response.json()
          print(f" Upload failed: {error_data.get('message', 'Unknown error')}")
          if 'details' in error_data:
              print(f"Details: {error_data['details']}")
      except Exception as error:
          print(f" Error: {str(error)}")
      finally:
          # Close all file handles
          for file in files.values():
              file.close()

  if __name__ == '__main__':
      upload_onboarding_files()
  ```

  ```php PHP theme={null}
  <?php

  $bearerToken = getenv('BEARER_TOKEN');
  $merchantId = '12345';

  $url = "https://kyc.sbx.moduluslabs.io/v2/onboard/{$merchantId}/files";

  // Prepare file data
  $files = [
      'idsOfValidSignatories' => new CURLFile('/path/to/valid_id_front.jpg', 'image/jpeg', 'valid_id_front.jpg'),
      'idsOfValidSignatoriesBack' => new CURLFile('/path/to/valid_id_back.jpg', 'image/jpeg', 'valid_id_back.jpg'),
      'mayorsOrBusinessPermit' => new CURLFile('/path/to/business_permit.pdf', 'application/pdf', 'business_permit.pdf'),
      'storePhoto' => new CURLFile('/path/to/store_photo.jpg', 'image/jpeg', 'store_photo.jpg')
  ];

  $ch = curl_init($url);
  curl_setopt($ch, CURLOPT_POST, true);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $files);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'Authorization: Bearer ' . $bearerToken
  ]);

  $response = curl_exec($ch);
  $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  curl_close($ch);

  $data = json_decode($response, true);

  if ($httpCode === 200) {
      echo " Files uploaded successfully!\n";
      echo "Uploaded " . count($data['files']) . " file(s)\n";

      foreach ($data['files'] as $file) {
          echo "  • {$file['fileType']}: " . ($file['name'] ?? 'N/A') . " (ID: {$file['id']})\n";
      }
  } else {
      echo " Upload failed: {$data['message']}\n";
      if (isset($data['details'])) {
          echo "Details: " . json_encode($data['details']) . "\n";
      }
  }
  ```

  ```java Java theme={null}
  import java.io.File;
  import java.io.IOException;
  import java.net.URI;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.nio.file.Files;
  import java.util.ArrayList;
  import java.util.List;
  import org.json.JSONObject;
  import org.json.JSONArray;

  public class UploadOnboardingFiles {
      private static final String BEARER_TOKEN = System.getenv("BEARER_TOKEN");
      private static final String MERCHANT_ID = "12345";

      public static void main(String[] args) throws IOException, InterruptedException {
          String boundary = "----WebKitFormBoundary" + System.currentTimeMillis();
          List<byte[]> parts = new ArrayList<>();

          // Add files to multipart body
          addFilePart(parts, boundary, "idsOfValidSignatories",
              new File("/path/to/valid_id_front.jpg"), "image/jpeg");
          addFilePart(parts, boundary, "idsOfValidSignatoriesBack",
              new File("/path/to/valid_id_back.jpg"), "image/jpeg");
          addFilePart(parts, boundary, "mayorsOrBusinessPermit",
              new File("/path/to/business_permit.pdf"), "application/pdf");
          addFilePart(parts, boundary, "storePhoto",
              new File("/path/to/store_photo.jpg"), "image/jpeg");

          // Add closing boundary
          String closingBoundary = "\r\n--" + boundary + "--\r\n";
          parts.add(closingBoundary.getBytes());

          // Combine all parts
          byte[] body = combineBytes(parts);

          HttpClient client = HttpClient.newHttpClient();
          HttpRequest request = HttpRequest.newBuilder()
              .uri(URI.create("https://kyc.sbx.moduluslabs.io/v2/onboard/" + MERCHANT_ID + "/files"))
              .header("Authorization", "Bearer " + BEARER_TOKEN)
              .header("Content-Type", "multipart/form-data; boundary=" + boundary)
              .POST(HttpRequest.BodyPublishers.ofByteArray(body))
              .build();

          try {
              HttpResponse<String> response = client.send(request,
                  HttpResponse.BodyHandlers.ofString());

              if (response.statusCode() == 200) {
                  JSONObject data = new JSONObject(response.body());
                  JSONArray files = data.getJSONArray("files");

                  System.out.println(" Files uploaded successfully!");
                  System.out.println("Uploaded " + files.length() + " file(s)");

                  for (int i = 0; i < files.length(); i++) {
                      JSONObject file = files.getJSONObject(i);
                      System.out.println("  • " + file.getString("fileType") + ": " +
                          file.optString("name", "N/A") + " (ID: " + file.getString("id") + ")");
                  }
              } else {
                  JSONObject error = new JSONObject(response.body());
                  System.err.println(" Upload failed: " + error.getString("message"));
                  if (error.has("details")) {
                      System.err.println("Details: " + error.get("details"));
                  }
              }
          } catch (Exception e) {
              System.err.println(" Error: " + e.getMessage());
              e.printStackTrace();
          }
      }

      private static void addFilePart(List<byte[]> parts, String boundary,
                                     String fieldName, File file, String mimeType)
                                     throws IOException {
          String header = "--" + boundary + "\r\n" +
              "Content-Disposition: form-data; name=\"" + fieldName +
              "\"; filename=\"" + file.getName() + "\"\r\n" +
              "Content-Type: " + mimeType + "\r\n\r\n";

          parts.add(header.getBytes());
          parts.add(Files.readAllBytes(file.toPath()));
          parts.add("\r\n".getBytes());
      }

      private static byte[] combineBytes(List<byte[]> parts) {
          int totalLength = parts.stream().mapToInt(arr -> arr.length).sum();
          byte[] result = new byte[totalLength];
          int offset = 0;
          for (byte[] part : parts) {
              System.arraycopy(part, 0, result, offset, part.length);
              offset += part.length;
          }
          return result;
      }
  }
  ```

  ```go Go theme={null}
  package main

  import (
      "bytes"
      "encoding/json"
      "fmt"
      "io"
      "mime/multipart"
      "net/http"
      "os"
      "path/filepath"
  )

  type FileMetadata struct {
      ID       string `json:"id"`
      Name     string `json:"name,omitempty"`
      FileType string `json:"fileType"`
  }

  type UploadResponse struct {
      Files []FileMetadata `json:"files"`
  }

  type ErrorResponse struct {
      StatusCode int         `json:"statusCode"`
      Message    string      `json:"message"`
      Error      string      `json:"error"`
      Details    interface{} `json:"details,omitempty"`
  }

  func main() {
      bearerToken := os.Getenv("BEARER_TOKEN")
      merchantID := "12345"

      // Prepare files to upload
      filePaths := map[string]string{
          "idsOfValidSignatories":     "/path/to/valid_id_front.jpg",
          "idsOfValidSignatoriesBack": "/path/to/valid_id_back.jpg",
          "mayorsOrBusinessPermit":    "/path/to/business_permit.pdf",
          "storePhoto":                "/path/to/store_photo.jpg",
      }

      body := &bytes.Buffer{}
      writer := multipart.NewWriter(body)

      // Add files to multipart form
      for fieldName, filePath := range filePaths {
          file, err := os.Open(filePath)
          if err != nil {
              fmt.Printf(" Error opening file %s: %v\n", filePath, err)
              return
          }
          defer file.Close()

          part, err := writer.CreateFormFile(fieldName, filepath.Base(filePath))
          if err != nil {
              fmt.Printf(" Error creating form file: %v\n", err)
              return
          }

          _, err = io.Copy(part, file)
          if err != nil {
              fmt.Printf(" Error copying file content: %v\n", err)
              return
          }
      }

      writer.Close()

      url := fmt.Sprintf("https://kyc.sbx.moduluslabs.io/v2/onboard/%s/files", merchantID)
      req, err := http.NewRequest("POST", url, body)
      if err != nil {
          fmt.Printf(" Error creating request: %v\n", err)
          return
      }

      req.Header.Set("Authorization", "Bearer "+bearerToken)
      req.Header.Set("Content-Type", writer.FormDataContentType())

      client := &http.Client{}
      resp, err := client.Do(req)
      if err != nil {
          fmt.Printf(" Error making request: %v\n", err)
          return
      }
      defer resp.Body.Close()

      responseBody, err := io.ReadAll(resp.Body)
      if err != nil {
          fmt.Printf(" Error reading response: %v\n", err)
          return
      }

      if resp.StatusCode == 200 {
          var uploadResponse UploadResponse
          if err := json.Unmarshal(responseBody, &uploadResponse); err != nil {
              fmt.Printf(" Error parsing response: %v\n", err)
              return
          }

          fmt.Println(" Files uploaded successfully!")
          fmt.Printf("Uploaded %d file(s)\n", len(uploadResponse.Files))

          for _, file := range uploadResponse.Files {
              name := file.Name
              if name == "" {
                  name = "N/A"
              }
              fmt.Printf("  • %s: %s (ID: %s)\n", file.FileType, name, file.ID)
          }
      } else {
          var errorResponse ErrorResponse
          if err := json.Unmarshal(responseBody, &errorResponse); err != nil {
              fmt.Printf(" Error parsing error response: %v\n", err)
              return
          }

          fmt.Printf(" Upload failed: %s\n", errorResponse.Message)
          if errorResponse.Details != nil {
              fmt.Printf("Details: %v\n", errorResponse.Details)
          }
      }
  }
  ```

  ```csharp C#/.NET theme={null}
  using System;
  using System.IO;
  using System.Net.Http;
  using System.Net.Http.Headers;
  using System.Text.Json;
  using System.Threading.Tasks;

  class Program
  {
      private static readonly string BEARER_TOKEN = Environment.GetEnvironmentVariable("BEARER_TOKEN");
      private static readonly string MERCHANT_ID = "12345";

      static async Task Main(string[] args)
      {
          try
          {
              using var client = new HttpClient();
              client.DefaultRequestHeaders.Authorization =
                  new AuthenticationHeaderValue("Bearer", BEARER_TOKEN);

              using var content = new MultipartFormDataContent();

              // Add files to multipart form
              var files = new Dictionary<string, string>
              {
                  { "idsOfValidSignatories", "/path/to/valid_id_front.jpg" },
                  { "idsOfValidSignatoriesBack", "/path/to/valid_id_back.jpg" },
                  { "mayorsOrBusinessPermit", "/path/to/business_permit.pdf" },
                  { "storePhoto", "/path/to/store_photo.jpg" }
              };

              foreach (var file in files)
              {
                  var fileContent = new ByteArrayContent(File.ReadAllBytes(file.Value));
                  fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(
                      GetMimeType(file.Value));
                  content.Add(fileContent, file.Key, Path.GetFileName(file.Value));
              }

              var response = await client.PostAsync(
                  $"https://kyc.sbx.moduluslabs.io/v2/onboard/{MERCHANT_ID}/files",
                  content
              );

              var responseBody = await response.Content.ReadAsStringAsync();

              if (response.IsSuccessStatusCode)
              {
                  using var doc = JsonDocument.Parse(responseBody);
                  var filesArray = doc.RootElement.GetProperty("files");

                  Console.WriteLine(" Files uploaded successfully!");
                  Console.WriteLine($"Uploaded {filesArray.GetArrayLength()} file(s)");

                  foreach (var fileElement in filesArray.EnumerateArray())
                  {
                      var id = fileElement.GetProperty("id").GetString();
                      var fileType = fileElement.GetProperty("fileType").GetString();
                      var name = fileElement.TryGetProperty("name", out var nameElement)
                          ? nameElement.GetString()
                          : "N/A";

                      Console.WriteLine($"  • {fileType}: {name} (ID: {id})");
                  }
              }
              else
              {
                  using var doc = JsonDocument.Parse(responseBody);
                  var message = doc.RootElement.GetProperty("message").GetString();

                  Console.WriteLine($" Upload failed: {message}");

                  if (doc.RootElement.TryGetProperty("details", out var details))
                  {
                      Console.WriteLine($"Details: {details}");
                  }
              }
          }
          catch (Exception ex)
          {
              Console.WriteLine($" Error: {ex.Message}");
          }
      }

      private static string GetMimeType(string filePath)
      {
          var extension = Path.GetExtension(filePath).ToLowerInvariant();
          return extension switch
          {
              ".jpg" or ".jpeg" => "image/jpeg",
              ".png" => "image/png",
              ".pdf" => "application/pdf",
              _ => "application/octet-stream"
          };
      }
  }
  ```
</RequestExample>

## Response

### Success Response

**Status Code:** `200 OK`

<ResponseField name="files" type="array" required>
  List of successfully uploaded files with their metadata

  <Expandable title="File Object Properties">
    <ResponseField name="id" type="string" required>
      Unique identifier (UUID v4) assigned to the file in S3 storage

      **Format:** UUID v4

      **Example:** `"550e8400-e29b-41d4-a716-446655440000"`
    </ResponseField>

    <ResponseField name="name" type="string">
      Sanitized original file name

      **Example:** `"valid_id_front.jpg"`
    </ResponseField>

    <ResponseField name="fileType" type="string" required>
      Category/type of the uploaded file

      **Values:** `idsOfValidSignatories`, `idsOfValidSignatoriesBack`, `idsOfValidSignatoriesSignature`, `mayorsOrBusinessPermit`, `mayorsOrBusinessPermitBack`, `mayorsOrBusinessPermitSignature`, `birCert2303`, `birCert2303Back`, `birCert2303Signature`, `secCert`, `secCertBack`, `secCertSignature`, `gis`, `gisBack`, `gisSignature`, `aoiAndByLaws`, `aoiAndByLawsBack`, `aoiAndByLawsSignature`, `secretarysCert`, `secretarysCertBack`, `secretarysCertSignature`, `barangayBusinessPermit`, `barangayBusinessPermitBack`, `barangayBusinessPermitSignature`, `storePhoto`
    </ResponseField>
  </Expandable>
</ResponseField>

```json theme={null}
{
  "files": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "valid_id_front.jpg",
      "fileType": "idsOfValidSignatories"
    },
    {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "name": "valid_id_back.jpg",
      "fileType": "idsOfValidSignatoriesBack"
    },
    {
      "id": "550e8400-e29b-41d4-a716-446655440002",
      "name": "business_permit.pdf",
      "fileType": "mayorsOrBusinessPermit"
    }
  ]
}
```

### Error Responses

<AccordionGroup>
  <Accordion title="400 Bad Request - Invalid Merchant ID">
    **Status Code:** `400`

    ```json theme={null}
    {
      "statusCode": 400,
      "message": "Invalid merchant ID",
      "error": "Bad Request",
      "details": {
        "merchantIdReceived": "abc"
      }
    }
    ```

    **Cause:** The merchantId in the URL is not a valid numeric value

    **Solution:** Ensure merchantId contains only digits
  </Accordion>

  <Accordion title="400 Bad Request - Merchant ID Mismatch">
    **Status Code:** `400`

    ```json theme={null}
    {
      "statusCode": 400,
      "message": "Merchant ID mismatch",
      "error": "Bad Request"
    }
    ```

    **Cause:** Regular user attempting to upload files for a different merchant account

    **Solution:** Use the correct merchantId that matches your authenticated account, or use Super Admin credentials
  </Accordion>

  <Accordion title="400 Bad Request - No Files Received">
    **Status Code:** `400`

    ```json theme={null}
    {
      "statusCode": 400,
      "message": "No files received",
      "error": "Bad Request"
    }
    ```

    **Cause:** Request didn't include any files

    **Solution:** Include at least one file in the multipart/form-data request
  </Accordion>

  <Accordion title="400 Bad Request - Invalid File Content Type">
    **Status Code:** `400`

    ```json theme={null}
    {
      "statusCode": 400,
      "message": "Invalid file content type",
      "error": "Bad Request",
      "details": {
        "fileContentTypeReceived": "application/x-msdownload"
      }
    }
    ```

    **Cause:** File's actual content (detected via magic bytes) is not JPEG, PNG, or PDF

    **Solution:** Only upload files with valid image or PDF content
  </Accordion>

  <Accordion title="400 Bad Request - Invalid File MIME Type">
    **Status Code:** `400`

    ```json theme={null}
    {
      "statusCode": 400,
      "message": "Invalid file MIME type",
      "error": "Bad Request",
      "details": {
        "mimeTypeReceived": "application/octet-stream"
      }
    }
    ```

    **Cause:** File's MIME type header doesn't match allowed types

    **Solution:** Ensure Content-Type header is image/jpeg, image/png, or application/pdf
  </Accordion>

  <Accordion title="400 Bad Request - Invalid File Extension">
    **Status Code:** `400`

    ```json theme={null}
    {
      "statusCode": 400,
      "message": "Invalid file extension",
      "error": "Bad Request",
      "details": {
        "fileExtensionReceived": ".exe"
      }
    }
    ```

    **Cause:** File extension is not .jpg, .jpeg, .png, or .pdf

    **Solution:** Only upload files with allowed extensions
  </Accordion>

  <Accordion title="400 Bad Request - File Name Too Long">
    **Status Code:** `400`

    ```json theme={null}
    {
      "statusCode": 400,
      "message": "File name too long",
      "error": "Bad Request",
      "details": {
        "fileNameLengthReceived": 300
      }
    }
    ```

    **Cause:** File name exceeds 255 characters

    **Solution:** Rename file to have a shorter name (max 255 characters)
  </Accordion>

  <Accordion title="400 Bad Request - File Size Too Large">
    **Status Code:** `400`

    ```json theme={null}
    {
      "statusCode": 400,
      "message": "File size exceeds 25MB limit",
      "error": "Bad Request"
    }
    ```

    **Cause:** File size exceeds 25MB limit

    **Solution:** Reduce file size or split into multiple files
  </Accordion>

  <Accordion title="400 Bad Request - Failed to Save">
    **Status Code:** `400`

    ```json theme={null}
    {
      "statusCode": 400,
      "message": "Failed to save merchant's VALID_IDS_OF_SIGNATORY file/s",
      "error": "Bad Request"
    }
    ```

    **Cause:** Error saving files to S3 or database

    **Solution:** Retry the request. If issue persists, contact support
  </Accordion>

  <Accordion title="401 Unauthorized">
    **Status Code:** `401`

    ```json theme={null}
    {
      "statusCode": 401,
      "message": "Account not found",
      "error": "Unauthorized"
    }
    ```

    **Cause:** Invalid or missing JWT Bearer token

    **Solution:** Ensure valid authentication token is provided in Authorization header
  </Accordion>

  <Accordion title="429 Too Many Requests">
    **Status Code:** `429`

    ```json theme={null}
    {
      "statusCode": 429,
      "message": "ThrottlerException: Too Many Requests"
    }
    ```

    **Cause:** Rate limit exceeded (more than 5 requests in 60 seconds)

    **Solution:** Wait 60 seconds before retrying. Implement exponential backoff in your application
  </Accordion>
</AccordionGroup>

## Security Features

This endpoint implements OWASP File Upload Cheat Sheet best practices:

<CardGroup cols={2}>
  <Card title="Extension Validation" icon="file-check">
    Only .jpg, .jpeg, .png, .pdf extensions allowed
  </Card>

  <Card title="MIME Type Validation" icon="shield-check">
    Validates Content-Type header matches allowed types
  </Card>

  <Card title="Magic Bytes Validation" icon="fingerprint">
    Uses file-type library to detect actual file content
  </Card>

  <Card title="File Size Limits" icon="scale-balanced">
    Maximum 25MB per file to prevent DoS attacks
  </Card>

  <Card title="File Name Sanitization" icon="broom">
    Strips dangerous characters from file names
  </Card>

  <Card title="Secure Storage" icon="lock">
    Files stored in S3 with UUID naming to prevent path traversal
  </Card>
</CardGroup>

## Best Practices

<AccordionGroup>
  <Accordion title="Upload Files Before Submitting Onboarding" icon="list-check">
    Upload all required documents before calling the [Onboard Merchant](/api-reference/onboarding/onboard-merchant) endpoint:

    ```javascript theme={null}
    // 1. Upload files first
    const uploadResponse = await uploadOnboardingFiles(merchantId, files);

    // 2. Extract file IDs
    const fileIds = uploadResponse.files.map(f => f.id);

    // 3. Submit onboarding with file references
    const onboardingData = {
      merchantName: 'Acme Store',
      // ... other fields
      documentIds: fileIds
    };

    await onboardMerchant(onboardingData);
    ```
  </Accordion>

  <Accordion title="Validate Files Client-Side First" icon="magnifying-glass">
    Check files before uploading to avoid unnecessary API calls:

    ```javascript theme={null}
    function validateFile(file) {
      const errors = [];

      // Check file size (25MB = 26214400 bytes)
      if (file.size > 26214400) {
        errors.push('File exceeds 25MB limit');
      }

      // Check file extension
      const allowedExtensions = ['.jpg', '.jpeg', '.png', '.pdf'];
      const ext = '.' + file.name.split('.').pop().toLowerCase();
      if (!allowedExtensions.includes(ext)) {
        errors.push('Invalid file extension. Only JPG, PNG, PDF allowed');
      }

      // Check file name length
      if (file.name.length > 255) {
        errors.push('File name too long (max 255 characters)');
      }

      return errors;
    }
    ```
  </Accordion>

  <Accordion title="Handle Rate Limiting Gracefully" icon="clock">
    Implement retry logic with exponential backoff:

    ```javascript theme={null}
    async function uploadWithRetry(merchantId, files, maxRetries = 3) {
      let retries = 0;

      while (retries < maxRetries) {
        try {
          return await uploadOnboardingFiles(merchantId, files);
        } catch (error) {
          if (error.response?.status === 429) {
            const waitTime = Math.pow(2, retries) * 1000; // Exponential backoff
            console.log(`Rate limited. Waiting ${waitTime}ms before retry...`);
            await new Promise(resolve => setTimeout(resolve, waitTime));
            retries++;
          } else {
            throw error;
          }
        }
      }

      throw new Error('Max retries exceeded');
    }
    ```
  </Accordion>

  <Accordion title="Organize Files by Document Type" icon="folder-tree">
    Keep track of which files belong to which document category:

    ```javascript theme={null}
    const documentFiles = {
      validIds: {
        front: '/path/to/id_front.jpg',
        back: '/path/to/id_back.jpg'
      },
      businessPermit: {
        front: '/path/to/permit.pdf'
      },
      storePhoto: '/path/to/store.jpg'
    };

    // Map to form field names
    const formData = new FormData();
    formData.append('idsOfValidSignatories', documentFiles.validIds.front);
    formData.append('idsOfValidSignatoriesBack', documentFiles.validIds.back);
    formData.append('mayorsOrBusinessPermit', documentFiles.businessPermit.front);
    formData.append('storePhoto', documentFiles.storePhoto);
    ```
  </Accordion>

  <Accordion title="Compress Images Before Upload" icon="compress">
    Reduce file sizes while maintaining quality:

    ```javascript theme={null}
    async function compressImage(file, maxSizeMB = 5) {
      const options = {
        maxSizeMB: maxSizeMB,
        maxWidthOrHeight: 1920,
        useWebWorker: true,
        fileType: file.type
      };

      try {
        const compressedFile = await imageCompression(file, options);
        console.log(`Compressed from ${file.size} to ${compressedFile.size}`);
        return compressedFile;
      } catch (error) {
        console.error('Compression failed:', error);
        return file; // Return original if compression fails
      }
    }
    ```
  </Accordion>

  <Accordion title="Show Upload Progress" icon="spinner">
    Provide feedback to users during upload:

    ```javascript theme={null}
    async function uploadWithProgress(merchantId, files, onProgress) {
      const formData = new FormData();

      Object.entries(files).forEach(([fieldName, file]) => {
        formData.append(fieldName, file);
      });

      const response = await axios.post(
        `https://kyc.sbx.moduluslabs.io/v2/onboard/${merchantId}/files`,
        formData,
        {
          headers: {
            'Authorization': `Bearer ${token}`,
          },
          onUploadProgress: (progressEvent) => {
            const percentCompleted = Math.round(
              (progressEvent.loaded * 100) / progressEvent.total
            );
            onProgress(percentCompleted);
          }
        }
      );

      return response.data;
    }

    // Usage
    uploadWithProgress(merchantId, files, (percent) => {
      console.log(`Upload progress: ${percent}%`);
      updateProgressBar(percent);
    });
    ```
  </Accordion>
</AccordionGroup>

## Use Cases

<CardGroup cols={2}>
  <Card title="Initial Document Upload" icon="upload">
    Upload required documents when first creating an onboarding application
  </Card>

  <Card title="Document Updates" icon="arrows-rotate">
    Replace or add documents after application has been declined
  </Card>

  <Card title="Additional Documents" icon="file-plus">
    Submit supplementary documents requested by compliance team
  </Card>

  <Card title="Document Corrections" icon="pen-to-square">
    Re-upload documents with better quality or correct information
  </Card>
</CardGroup>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Sending Strings Instead of Binary Files" icon="triangle-exclamation">
    **Error:** `No files received` or `Invalid file content type`

    **Issue:** Sending file paths, URLs, or base64-encoded strings instead of actual binary file data

    **Incorrect Examples:**

    ```javascript theme={null}
    //  WRONG - Sending file path as string
    formData.append('idsOfValidSignatories', '/path/to/file.jpg');

    //  WRONG - Sending base64 string
    formData.append('idsOfValidSignatories', 'data:image/jpeg;base64,/9j/4AAQ...');

    //  WRONG - Sending URL
    formData.append('idsOfValidSignatories', 'https://example.com/file.jpg');
    ```

    **Correct Examples:**

    ```javascript theme={null}
    //  CORRECT - Node.js with file stream
    const fs = require('fs');
    formData.append('idsOfValidSignatories',
      fs.createReadStream('/path/to/file.jpg'));

    //  CORRECT - Browser with File object
    const fileInput = document.getElementById('fileInput');
    formData.append('idsOfValidSignatories', fileInput.files[0]);

    //  CORRECT - Python with file handle
    files = {'idsOfValidSignatories': open('/path/to/file.jpg', 'rb')}

    //  CORRECT - PHP with CURLFile
    $file = new CURLFile('/path/to/file.jpg', 'image/jpeg', 'file.jpg');
    ```

    **Solution:** Always use proper file upload mechanisms that send binary data, not text representations
  </Accordion>

  <Accordion title="Magic Bytes Validation Failure" icon="file-slash">
    **Error:** `Invalid file content type`

    **Issue:** File extension doesn't match actual file content

    **Common Causes:**

    * Renamed file with wrong extension (e.g., .txt renamed to .jpg)
    * Corrupted file
    * File created by unsupported software

    **Solution:**

    * Use legitimate image editing or PDF software
    * Don't just rename file extensions
    * Verify file opens correctly before uploading
    * Try converting file to correct format using standard tools
  </Accordion>

  <Accordion title="File Too Large Even After Compression" icon="circle-exclamation">
    **Error:** `File size exceeds 25MB limit`

    **Solutions:**

    * For PDFs: Split multi-page documents across front/back/signature fields
    * For images: Reduce resolution (1920px width is usually sufficient)
    * For images: Convert to JPEG with 80-85% quality
    * For images: Use online compression tools like TinyPNG or Squoosh
    * Consider splitting document into multiple logical files
  </Accordion>

  <Accordion title="Rate Limit Issues During Bulk Upload" icon="gauge-high">
    **Error:** `429 Too Many Requests`

    **Issue:** Trying to upload files for multiple merchants quickly

    **Solutions:**

    * Implement rate limiting in your application (max 5 requests per 60s)
    * Add delays between merchant file uploads
    * Queue uploads and process with appropriate spacing
    * Use exponential backoff for retries

    ```javascript theme={null}
    const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

    async function uploadForMultipleMerchants(merchantFileMap) {
      const results = [];

      for (const [merchantId, files] of Object.entries(merchantFileMap)) {
        try {
          const result = await uploadOnboardingFiles(merchantId, files);
          results.push({ merchantId, success: true, data: result });
        } catch (error) {
          results.push({ merchantId, success: false, error: error.message });
        }

        // Wait 12 seconds between uploads (5 requests per 60s = 1 per 12s)
        await delay(12000);
      }

      return results;
    }
    ```
  </Accordion>

  <Accordion title="Multipart Form Data Encoding Issues" icon="code">
    **Issue:** Files not being received by server or showing as undefined

    **Solutions for Different Languages:**

    **Node.js:**

    ```javascript theme={null}
    // Use form-data package, not URLSearchParams
    const FormData = require('form-data');
    const form = new FormData();
    form.append('idsOfValidSignatories', fs.createReadStream(filePath));

    // Include form headers
    axios.post(url, form, {
      headers: {
        'Authorization': `Bearer ${token}`,
        ...form.getHeaders()  // Important!
      }
    });
    ```

    **Python:**

    ```python theme={null}
    # Let requests handle Content-Type header
    files = {'idsOfValidSignatories': open(file_path, 'rb')}
    headers = {'Authorization': f'Bearer {token}'}
    # Don't set Content-Type manually
    requests.post(url, headers=headers, files=files)
    ```

    **PHP:**

    ```php theme={null}
    // Use CURLFile, not @ syntax (deprecated)
    $file = new CURLFile($filePath, 'image/jpeg', 'filename.jpg');
    ```
  </Accordion>

  <Accordion title="File Name Sanitization Concerns" icon="shield">
    **Issue:** Uploaded file name doesn't match original

    **Explanation:** File names are sanitized for security:

    * Special characters removed
    * Path traversal sequences removed (../, ..)
    * Length truncated to 255 characters

    **Best Practice:**

    * Use simple, descriptive file names
    * Avoid special characters
    * Use the returned `id` field to reference files, not `name`
    * Store your own mapping of file IDs to original names if needed
  </Accordion>
</AccordionGroup>

## Document Checklist

Use this checklist to ensure you have all required documents before uploading:

<Tabs>
  <Tab title="STARTER">
    * [ ] Valid ID(s) of Signatory
    * [ ] Photo of store with store name
    * [ ] Barangay Business Permit
    * [ ] Incorporator's Government Issued ID
    * [ ] Signatory's Government Issued ID
    * [ ] Authorized Representative Government Issued ID
  </Tab>

  <Tab title="SOLE_PROPRIETOR">
    * [ ] Valid ID(s) of Signatory
    * [ ] Incorporator's Government Issued ID
    * [ ] Signatory's Government Issued ID
    * [ ] Authorized Representative Government Issued ID
    * [ ] **Either of the two:**
      * [ ] Business / Mayor's Permit **OR**
      * [ ] BIR Certificate of Registration (2303)
  </Tab>

  <Tab title="PARTNERSHIP">
    * [ ] Valid ID(s) of Signatory
    * [ ] Incorporator's Government Issued ID
    * [ ] Signatory's Government Issued ID
    * [ ] Authorized Representative Government Issued ID
    * [ ] List of Goods/Services sold with Pricing
    * [ ] Refund and Refund Policy
    * [ ] Fulfillment Policy
    * [ ] Privacy Policy
    * [ ] **At least one of the following:**
      * [ ] Business / Mayor's Permit **OR**
      * [ ] BIR Certificate of Registration (2303) **OR**
      * [ ] SEC Registration Certificate
    * [ ] **Optional files (not mandatory but can be uploaded):**
      * [ ] AOI and By Laws
      * [ ] Secretary's Certificate
  </Tab>

  <Tab title="CORPORATION">
    * [ ] Valid ID(s) of Signatory
    * [ ] Incorporator's Government Issued ID
    * [ ] Signatory's Government Issued ID
    * [ ] Authorized Representative Government Issued ID
    * [ ] List of Goods/Services sold with Pricing
    * [ ] Refund and Refund Policy
    * [ ] Fulfillment Policy
    * [ ] Privacy Policy
    * [ ] **At least one of the following:**
      * [ ] Mayor's or Business Permit **OR**
      * [ ] BIR Certificate 2303 **OR**
      * [ ] SEC Certificate
    * [ ] **Either of the two:**
      * [ ] Article of Partnership/Incorporation (AOI) and By-Laws **OR**
      * [ ] General Information Sheet (GIS)
    * [ ] Secretary's Certificate
  </Tab>
</Tabs>

## Next Steps

<CardGroup cols={2}>
  <Card title="Onboard Merchant" icon="user-plus" href="/api-reference/onboarding/onboard-merchant">
    Submit onboarding application after uploading files
  </Card>

  <Card title="Retrieve Onboarding Status" icon="clipboard-check" href="/api-reference/onboarding/retrieve-onboarding-status">
    Check onboarding application status
  </Card>

  <Card title="Update Onboarding Data" icon="pen-to-square" href="/api-reference/onboarding/update-onboarding-data">
    Update declined application with corrected documents
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation" href="/docs/errors">
    Complete error code reference
  </Card>
</CardGroup>


## OpenAPI

````yaml POST /v2/onboard/{merchantId}/files
openapi: 3.1.0
info:
  title: Modulus Labs Onboarding API
  description: API for merchant onboarding, KYC verification, and account management
  version: 2.0.0
servers:
  - url: https://kyc.sbx.moduluslabs.io
    description: Sandbox
security: []
paths:
  /v2/onboard/{merchantId}/files:
    post:
      tags:
        - Onboarding
      summary: Upload Onboarding Files
      description: >-
        Upload business documents for merchant onboarding verification. Accepts
        multiple document types including valid IDs, business permits,
        certificates, and supporting documents.
      operationId: uploadOnboardingFiles
      parameters:
        - name: merchantId
          in: path
          required: true
          description: The merchant's account ID (numeric string)
          schema:
            type: string
            pattern: ^\d+$
          example: '12345'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/UploadFilesRequest'
      responses:
        '200':
          description: Files uploaded successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadFilesResponse'
        '400':
          description: >-
            Bad Request - Invalid file, merchant ID mismatch, or validation
            error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Too Many Requests - Rate limit exceeded (5 requests per 60 seconds)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearerAuth: []
components:
  schemas:
    UploadFilesRequest:
      type: object
      properties:
        idsOfValidSignatories:
          type: string
          format: binary
          description: Valid government-issued IDs of authorized signatories (front side)
        idsOfValidSignatoriesBack:
          type: string
          format: binary
          description: Back side of valid IDs
        idsOfValidSignatoriesSignature:
          type: string
          format: binary
          description: Signature specimen from valid IDs
        mayorsOrBusinessPermit:
          type: string
          format: binary
          description: Mayor's Permit or Business Permit (front)
        mayorsOrBusinessPermitBack:
          type: string
          format: binary
          description: Back side of Mayor's/Business Permit
        mayorsOrBusinessPermitSignature:
          type: string
          format: binary
          description: Signature page of Mayor's/Business Permit
        birCert2303:
          type: string
          format: binary
          description: BIR Certificate of Registration (Form 2303) - front
        birCert2303Back:
          type: string
          format: binary
          description: Back side of BIR Certificate 2303
        birCert2303Signature:
          type: string
          format: binary
          description: Signature page of BIR Certificate 2303
        secCert:
          type: string
          format: binary
          description: SEC Certificate - front
        secCertBack:
          type: string
          format: binary
          description: Back side of SEC Certificate
        secCertSignature:
          type: string
          format: binary
          description: Signature page of SEC Certificate
        gis:
          type: string
          format: binary
          description: General Information Sheet - front
        gisBack:
          type: string
          format: binary
          description: Back side of GIS
        gisSignature:
          type: string
          format: binary
          description: Signature page of GIS
        aoiAndByLaws:
          type: string
          format: binary
          description: Articles of Incorporation and By-Laws - front
        aoiAndByLawsBack:
          type: string
          format: binary
          description: Back side of AOI and By-Laws
        aoiAndByLawsSignature:
          type: string
          format: binary
          description: Signature page of AOI and By-Laws
        secretarysCert:
          type: string
          format: binary
          description: Secretary's Certificate - front
        secretarysCertBack:
          type: string
          format: binary
          description: Back side of Secretary's Certificate
        secretarysCertSignature:
          type: string
          format: binary
          description: Signature page of Secretary's Certificate
        barangayBusinessPermit:
          type: string
          format: binary
          description: Barangay Business Permit - front
        barangayBusinessPermitBack:
          type: string
          format: binary
          description: Back side of Barangay Business Permit
        barangayBusinessPermitSignature:
          type: string
          format: binary
          description: Signature page of Barangay Business Permit
        storePhoto:
          type: string
          format: binary
          description: >-
            Photo of the store/business establishment (must show visible store
            name)
    UploadFilesResponse:
      type: object
      properties:
        files:
          type: array
          items:
            $ref: '#/components/schemas/UploadedFile'
          description: List of successfully uploaded files
    ErrorResponse:
      type: object
      properties:
        statusCode:
          type: integer
          description: HTTP status code
        message:
          type: string
          description: Error message
        error:
          type: string
          description: Error type
        details:
          type: object
          description: Additional error details
    UploadedFile:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier assigned to the file
        name:
          type: string
          description: Sanitized original file name
        fileType:
          type: string
          description: Category/type of the uploaded file
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT Bearer token authentication

````