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

> ## Agent Instructions
> Describe only the public 123hub merchant API documented on this site. The primary endpoint is POST /public/api/multihub/v1.
> Preserve API method names, field names, header names, and error codes exactly as documented. Do not invent endpoints or parameters.
> Treat every amount as an integer in minor units unless a page explicitly states otherwise.
> Never expose, request, or fabricate a merchant secret key. The SDK pages contain reference implementations, not official SDK packages.

# Support attachments

> Upload, verify, attach, and safely download public support evidence

# Support attachments

Public support files are uploaded directly to a quarantine object through a short-lived presigned POST. The platform validates size, checksum, extension, MIME type, magic bytes, and malware status before the file can be attached.

<Info>
  **Prerequisites:** use `tickets.create` for purpose `ticket` or
  `tickets.comment` for purpose `ticket_comment`. Reading ticket files requires
  `tickets.read`; reading chargeback evidence requires `chargebacks.read`.
</Info>

## Constraints

| Constraint                          | Value                                                                   |
| ----------------------------------- | ----------------------------------------------------------------------- |
| Maximum files on one ticket/comment | 10 unique IDs                                                           |
| Maximum size per file               | 10 MiB (`10485760` bytes)                                               |
| Upload session TTL                  | Up to 15 minutes                                                        |
| Download URL TTL                    | Up to 5 minutes                                                         |
| Allowed MIME types                  | `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `application/pdf` |
| Required checksum                   | 64-character SHA-256 hex digest                                         |

The declared name, MIME type, exact byte size, and checksum are immutable for the session. Use purpose `ticket` only for ticket creation and `ticket_comment` only for comment creation.

## Flow

1. Calculate the local file byte length and SHA-256.
2. Call `attachment.prepare` with a stable top-level `operation_id`.
3. Submit every field returned in `result.upload.fields` plus the file to `result.upload.url`.
4. Call `attachment.finalize` with another stable `operation_id`.
5. Wait for status `ready`; only then include the attachment `h_id` in a ticket/comment mutation.

## Prepare and upload

<CodeGroup>
  ```bash cURL theme={null}
  FILE='./receipt.pdf'
  SIZE=$(wc -c < "$FILE" | tr -d ' ')
  SHA256=$(openssl dgst -sha256 -r "$FILE" | awk '{print $1}')
  BODY=$(printf '{"method":"attachment.prepare","operation_id":"receipt-8401-prepare","params":{"purpose":"ticket","name":"receipt.pdf","mime_type":"application/pdf","size":%s,"sha256":"%s"}}' "$SIZE" "$SHA256")
  SECRET='your_secret_key'
  HASH=$(printf '%s%s' "$BODY" "$SECRET" | openssl dgst -sha512 -r | awk '{print $1}')

  curl -X POST 'https://api.bafanglaicai88.com/public/api/multihub/v1' \
    -H 'Content-Type: application/json' -H 'X-Data-Application-Id: 1' \
    -H "X-Data-Hash: $HASH" -H "X-Data-Timestamp: $(date +%s)" \
    -H "X-Data-Nonce: $(uuidgen)" --data-binary "$BODY"

  # POST every returned upload.fields entry unchanged, then add: -F "file=@${FILE};type=application/pdf"
  ```

  ```python Python theme={null}
  import hashlib, json, time, uuid, requests

  path = "receipt.pdf"; content = open(path, "rb").read(); secret = "your_secret_key"
  payload = {"method": "attachment.prepare", "operation_id": "receipt-8401-prepare", "params": {
      "purpose": "ticket", "name": "receipt.pdf", "mime_type": "application/pdf",
      "size": len(content), "sha256": hashlib.sha256(content).hexdigest(),
  }}
  body = json.dumps(payload, separators=(",", ":"))
  prepared = requests.post("https://api.bafanglaicai88.com/public/api/multihub/v1", data=body, headers={
      "Content-Type": "application/json", "X-Data-Application-Id": "1",
      "X-Data-Hash": hashlib.sha512((body + secret).encode()).hexdigest(),
      "X-Data-Timestamp": str(int(time.time())), "X-Data-Nonce": str(uuid.uuid4()),
  }, timeout=30).json()["result"]
  requests.post(prepared["upload"]["url"], data=prepared["upload"]["fields"],
                files={"file": ("receipt.pdf", content, "application/pdf")}, timeout=60).raise_for_status()
  ```

  ```javascript Node.js theme={null}
  import crypto from "node:crypto";
  import { readFile } from "node:fs/promises";

  const content = await readFile("receipt.pdf");
  const secret = "your_secret_key";
  const body = JSON.stringify({
    method: "attachment.prepare",
    operation_id: "receipt-8401-prepare",
    params: {
      purpose: "ticket",
      name: "receipt.pdf",
      mime_type: "application/pdf",
      size: content.length,
      sha256: crypto.createHash("sha256").update(content).digest("hex"),
    },
  });
  const preparedResponse = await fetch(
    "https://api.bafanglaicai88.com/public/api/multihub/v1",
    {
      method: "POST",
      body,
      headers: {
        "Content-Type": "application/json",
        "X-Data-Application-Id": "1",
        "X-Data-Hash": crypto
          .createHash("sha512")
          .update(body + secret)
          .digest("hex"),
        "X-Data-Timestamp": String(Math.floor(Date.now() / 1000)),
        "X-Data-Nonce": crypto.randomUUID(),
      },
    },
  );
  const prepared = (await preparedResponse.json()).result;
  const form = new FormData();
  for (const [key, value] of Object.entries(prepared.upload.fields))
    form.append(key, value);
  form.append(
    "file",
    new Blob([content], { type: "application/pdf" }),
    "receipt.pdf",
  );
  const uploadResponse = await fetch(prepared.upload.url, {
    method: "POST",
    body: form,
  });
  if (!uploadResponse.ok)
    throw new Error(`Upload failed: ${uploadResponse.status}`);
  ```
</CodeGroup>

`attachment.prepare` returns:

```json theme={null}
{
  "identifiers": { "h_id": "e1924ea5-b980-49d5-a3fa-f2a34646bd4f" },
  "status": "pending",
  "expires_at": "2026-07-15T10:15:00.000Z",
  "upload": {
    "url": "https://storage.example/quarantine-post",
    "fields": {
      "key": "server-generated-value",
      "policy": "opaque-policy-value"
    }
  }
}
```

Treat every upload URL and field as opaque. The required `fields.key` value is a short-lived transport token, not an attachment identifier; it contains no merchant, ticket, chargeback, filename, or checksum data. Do not log, persist, parse, or replace it.

## Finalize and check status

Finalize after the object upload succeeds:

```json theme={null}
{
  "method": "attachment.finalize",
  "operation_id": "receipt-8401-finalize",
  "params": { "h_id": "e1924ea5-b980-49d5-a3fa-f2a34646bd4f" }
}
```

Finalization is idempotent. A malware scanner or storage outage fails closed with HTTP `503`; retry the same operation and attachment ID after backoff. Do not attach the file while scanning is incomplete.

Use `attachment.status` to observe `pending`, `scanning`, `ready`, or `consumed`:

```json theme={null}
{
  "method": "attachment.status",
  "params": { "h_id": "e1924ea5-b980-49d5-a3fa-f2a34646bd4f" }
}
```

`ready` means the ID can be attached exactly once to the matching purpose. `consumed` means it has already been claimed by its parent. Rejected and expired sessions return a stable attachment error rather than reusable upload data.

## Attach to a ticket or comment

Include only `ready` IDs:

```json theme={null}
{
  "method": "ticket.comment.create",
  "operation_id": "ticket-8401-comment-evidence-1",
  "params": {
    "ticket": { "c_id": "ticket-order-8401" },
    "body": "Attached the requested receipt.",
    "attachment_ids": ["e1924ea5-b980-49d5-a3fa-f2a34646bd4f"]
  }
}
```

An attachment cannot be reused for another ticket/comment, used across merchants, or attached under the wrong purpose. Unlinked ready files are cleaned up after 24 hours; expired and rejected objects are cleaned separately.

## Download

After rechecking the merchant, parent resource, scope, and internal visibility, `attachment.get` materializes an opaque storage alias and returns a five-minute URL for that alias:

```json theme={null}
{
  "method": "attachment.get",
  "params": { "h_id": "e1924ea5-b980-49d5-a3fa-f2a34646bd4f" }
}
```

```json theme={null}
{
  "identifiers": { "h_id": "e1924ea5-b980-49d5-a3fa-f2a34646bd4f" },
  "name": "receipt.pdf",
  "mime_type": "application/pdf",
  "size": 48219,
  "download_url": "https://storage.example/signed-download",
  "expires_at": "2026-07-15T10:20:00.000Z"
}
```

The API never signs a legacy/internal source key. The URL contains only an opaque alias and file bytes still move directly from object storage, not through the public API endpoint. Signed URLs must nevertheless be treated as secrets until they expire.

## Error handling

|    Code | HTTP | Meaning                                                         | Recovery                                              |
| ------: | :--: | --------------------------------------------------------------- | ----------------------------------------------------- |
| `13001` |  404 | Attachment or visible parent is not found                       | Refresh the parent attachment list                    |
| `13002` |  409 | Upload session expired                                          | Prepare a new session and upload again                |
| `13003` |  409 | File is scanning, unverified, wrong-purpose, or already claimed | Wait for `ready` or prepare a new valid file          |
| `13004` |  409 | Content failed validation or malware scan                       | Do not retry the same bytes; correct/replace the file |
|  `1013` |  409 | Prepare/finalize operation key payload changed                  | Retry the exact original operation                    |
|  `2002` |  503 | Storage or scanner unavailable                                  | Retry with the same operation key after backoff       |

## Best practices

* Calculate checksum and size from the exact bytes uploaded, not from a decoded/converted copy.
* Keep presigned form data out of logs and never send it to your frontend analytics.
* Verify a file reaches `ready` before creating the ticket/comment.
* Download through a freshly authorized `attachment.get` call rather than caching signed URLs.
