# AI Agent Integration Source: https://docs.bizmori.com/ai-agents Give Claude Code, Cursor, or Codex everything it needs to integrate the BIZ MORI API in one paste ## Copy this prompt Copy it, paste it into your agent, and fill in the `<...>` placeholders — service, stack, and your task. Leave the rest as is. It is deliberately short. Rather than restating the API, it does the three things an agent will not do on its own: **read the docs and report risks before writing code**, **name the mistakes to refuse to ship** (skipped confirm calls, unverified webhook signatures, hardcoded keys), and **prove the integration works** afterward instead of just claiming it does. ```text wrap theme={null} Integrate the BIZ MORI API for me. Docs: https://docs.bizmori.com Full docs: https://docs.bizmori.com/llms-full.txt <- guides, in plain text OpenAPI: https://docs.bizmori.com/api-reference/openapi.yaml <- authoritative schemas Base URL: https://api.bizmori.com Auth: Authorization: Bearer $BIZMORI_API_KEY (read from env; never hardcode or log it) Testing: use an sk_test_ key for the order lifecycle and webhook contract; use a live key for actual processing results Service: Stack: My task: Before writing any code, fetch BOTH files above: - llms-full.txt for the guides — read the quickstart for my service, plus Webhooks and Error Codes. - openapi.yaml for the exact request/response schemas. The guides do NOT contain the field definitions, so take every field name, type, enum value, and required/optional flag from the spec. Never infer a field from a code sample alone. Then tell me your plan and anything risky or ambiguous. Do not invent endpoints, fields, or parameters: if the spec does not have it, say so instead of guessing. If an endpoint is marked deprecated in the spec, do not use it — use the replacement it names. The docs specify these, and they are the things that get implemented wrong. Look each one up rather than assuming — do not ship code that skips them: - Orders are ASYNCHRONOUS, and the confirm step differs per service. Some services require a confirm call after upload; others have no confirm endpoint at all and must never receive one. Check which applies to my service before you write the flow. - Webhook signatures must be verified against the RAW request body, in constant time. - Presigned upload/download URLs expire, and there is an endpoint to refresh them. - Order creation takes an idempotency key. Reuse it when retrying. After you finish, verify — do not just claim it works: 1. With an `sk_test_` key, verify the upload, service-specific confirm step, and synthetic result state. State that processing did not run. For an actual end-to-end result, use a live API key and show the order reaching `complete`. 2. With a live key, show me the result actually retrieved (the downloaded file, or the detection field). With a test key, explain that no result file is created and download returns `PROCESSED_FILE_NOT_FOUND`. 3. Grep your own diff for a hardcoded key or a logged token, and show me the output. 4. If you built a webhook receiver, send it a bad signature and show it returns 401. If you could not run one of these, say so plainly instead of asserting it passed. ``` Use an `sk_test_` key for request, upload, state-transition, and webhook smoke tests. It does not run processing or consume credits, so use a live API key when you need to verify an actual completed result or download a file. ## Use it in your editor Paste the prompt straight into a session for a one-off task. To load it in every session, save the prompt to `.claude/bizmori-api.md` and import it from your `CLAUDE.md`: ```markdown CLAUDE.md theme={null} @.claude/bizmori-api.md ``` Claude Code reads imported files on startup, so the instructions are in context before you ask for anything. Save the prompt as a project rule so Cursor applies it whenever you touch integration code. Create `.cursor/rules/bizmori-api.mdc`: ```markdown theme={null} --- description: BIZ MORI API integration contract globs: ["**/*bizmori*", "**/api/**"] --- (paste the prompt here) ``` For a one-off task, paste it into the chat panel with `Cmd/Ctrl + L` instead. Codex and most other coding agents read an `AGENTS.md` at the repo root. Paste the prompt there under a `## BIZ MORI API` heading and it is picked up automatically on every run. For agents with no convention file, paste the prompt as the first message of the conversation. ## Give your agent the live docs The prompt above works because these docs are published in a machine-readable form. Your agent pulls the current spec at the moment you ask, rather than working from whatever was true when the prompt was written: | Resource | URL | Use it for | | -------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | OpenAPI spec | `https://docs.bizmori.com/api-reference/openapi.yaml` | Exact request/response schemas — field names, types, enums, required flags. Also generates a typed client | | Full docs | `https://docs.bizmori.com/llms-full.txt` | The guides as one plain-text file — flows, webhook verification, error codes | | Docs index | `https://docs.bizmori.com/llms.txt` | A compact map of every page, when the agent should navigate rather than ingest everything | | Any page as Markdown | append `.md` to a page URL | Feeding one specific guide into context | Both matter, and they are not interchangeable. `llms-full.txt` carries the guides but **not** the field definitions — those are rendered from the OpenAPI spec and do not survive into plain text. An agent given only `llms-full.txt` will reconstruct request bodies from code samples and guess the rest. That is why the prompt fetches both. ## Before you ship what the agent wrote AI agents produce plausible-looking code. Check these four things before shipping an integration — they are the mistakes we see most often. The key must come from an environment variable or a secret manager, never from source. Grep the diff for `Bearer ` and for anything resembling a literal key, and confirm the key is not written to logs. Anti-AI (upload mode) and AI Detection require a confirm call. Watermark Embed and Watermark Extract do not — and calling confirm for them will fail. Anti-AI in pure URL mode also skips confirm. The HMAC must be computed over the raw request bytes. If a JSON body-parser runs first and the code re-serializes the object, the signature will not match and the agent may be tempted to "fix" it by skipping verification. It must compare in constant time. Upload URLs are valid for 1 hour; download URLs for 7 days. A long-running batch job must call [Refresh URLs](/api-reference/orders/refresh-urls) rather than reusing a stale URL. ## Next steps Walk a flow by hand before automating it. The authoritative spec for every endpoint. Signature verification and event payloads in full. Every code your agent's error handling should cover. # Confirm AI Detection order Source: https://docs.bizmori.com/api-reference/ai-detection/confirm-order api-reference/openapi.yaml POST /api/v2/orders/ai-detection/confirm Confirms that the image upload is complete and starts processing. Call this endpoint after uploading the image to the upload URL received from the create order endpoint. The order status will change from `pending` to `inProgress`. The detection result is delivered via webhook (`order.aiDetection.completed` or `order.aiDetection.failed`), or can be retrieved by polling the [Get order](/api-reference/orders/get-order) endpoint. Call this endpoint only **after** uploading the image to the URL from the [Create order](/api-reference/ai-detection/create-order) step. Live keys use the presigned storage URL; test keys use the BIZ MORI test upload URL. If the file has not been uploaded yet, the request will return an error. # Create AI Detection order Source: https://docs.bizmori.com/api-reference/ai-detection/create-order api-reference/openapi.yaml POST /api/v2/orders/ai-detection Creates an AI Detection order and issues an upload URL for the image. Live keys receive an S3 presigned URL; test keys receive an absolute BIZ MORI test-upload URL. ## Flow 1. Call this endpoint to receive an upload URL 2. Upload the image using the live S3 URL or test BIZ MORI URL 3. Call `POST /api/v2/orders/ai-detection/confirm` to start async detection 4. Use webhooks or poll the order detail endpoint for the result ## Order Status | Status | Description | |--------|-------------| | `pending` | Waiting for file upload | | `inProgress` | Detection is in progress | | `complete` | Detection complete, result available | | `failed` | Detection failed | ## Supported Formats jpg, jpeg, png, webp, bmp, tiff ## How to upload the image After receiving the `uploadUrl` from the response, upload your image using a `PUT` request. Live keys return a presigned storage URL. Test keys return a BIZ MORI test upload URL that consumes and discards the request body: ```bash theme={null} curl -X PUT "UPLOAD_URL" \ -H "Content-Type: image/jpeg" \ --data-binary @photo.jpg ``` Live presigned upload URLs expire after **1 hour**. Test upload URLs carry their own authorization and do not require a Bearer header. Upload the file before the URL expires, then call the [Confirm order](/api-reference/ai-detection/confirm-order) endpoint. ## Getting the detection result Once you confirm the upload, the order status changes to `inProgress`. When detection completes, the result is available via: 1. **Webhook** — Subscribe to `order.aiDetection.completed` or `order.aiDetection.failed` events 2. **Polling** — Call the [Get order](/api-reference/orders/get-order) endpoint until status is `complete` or `failed` The detection result is included in the order detail response under `aiDetection`: ```json theme={null} { "data": { "orderId": "...", "status": "complete", "aiDetection": { "probability": 0.97, "statusCode": 1 } } } ``` Test orders return deterministic detection results from the input file name. Use `_ai` for probability `0.98` or `_human` for probability `0.02`. Test orders do not create downloadable heatmap or overlay files. ### Status Codes | `statusCode` | Meaning | | ------------ | --------------------------- | | `1` | Highly likely AI-generated | | `2` | Likely AI-generated | | `3` | Likely human-created | | `4` | Highly likely human-created | # Confirm Anti-AI order Source: https://docs.bizmori.com/api-reference/anti-ai/confirm-order api-reference/openapi.yaml POST /api/v2/orders/anti-ai/confirm Call after file upload is complete to start processing. - Call after all upload files are uploaded to S3 - Status changes to `inProgress` when processing starts - In mixed mode, only upload files are checked for S3 presence; URL files are already ready - Orders where all files are in URL mode are already `inProgress` and don't need confirm For an `sk_test_` upload-mode order, call this endpoint after the test upload succeeds. Anti-AI URL-mode orders start without an upload or confirm call. # Create Anti-AI order Source: https://docs.bizmori.com/api-reference/anti-ai/create-order api-reference/openapi.yaml POST /api/v2/orders/anti-ai Create an order for AI detection protection processing. ## Input Modes (Upload / URL / Mixed) The input mode is determined by the presence of `originalFileUrl` in each file object: - **Upload mode** (no `originalFileUrl`): Presigned URL issued → Client uploads to S3 → Call `/confirm` - **URL mode** (`originalFileUrl` provided): Image downloaded from the URL and processed immediately - **Mixed mode**: Some files in Upload mode, others in URL mode simultaneously ### Behavior | Condition | Order Status | Confirm Required | |-----------|-------------|-----------------| | All files have `originalFileUrl` | `inProgress` | No (immediate processing) | | Any file without `originalFileUrl` | `pending` | Yes (upload then confirm) | ## Zero Copy Mode When `mode.zeroCopy: true`, processed results are uploaded directly to customer-provided presigned URLs. In this case, `outputTargets` array is required and must match the length of `files` array. When you authenticate with an `sk_test_` key, the order follows the test lifecycle described in [Test API keys](/test-api-keys). Test uploads use the returned BIZ MORI upload URL, and no result file is created. ## Download order results Once your order reaches `complete` status, use the [Get download URL](/api-reference/orders/download-order) endpoint to retrieve a presigned download URL: ```bash theme={null} curl -X GET https://api.bizmori.com/api/v2/orders/{orderId}/download \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` Live order files are available for download for **up to 7 days** after order creation. An `sk_test_` order does not create a result file; its download request returns `404 PROCESSED_FILE_NOT_FOUND`. # Create Anti-AI order with URLs Source: https://docs.bizmori.com/api-reference/anti-ai/create-order-urls api-reference/openapi.yaml POST /api/v2/orders/anti-ai/with-urls **Deprecated**: Use `/anti-ai` endpoint with `originalFileUrl` field instead. Provide image URLs directly for processing. - Processing starts immediately without presigned URL issuance - Downloads and processes images from the provided URLs ## Zero Copy Mode When `mode.zeroCopy: true`, processed results are uploaded directly to customer-provided presigned URLs. # API Reference Source: https://docs.bizmori.com/api-reference/introduction Complete reference for the BIZ MORI API endpoints ## Base URL ``` https://api.bizmori.com ``` ## Authentication All endpoints require an API key passed via the `Authorization` header with Bearer format: ``` Authorization: Bearer YOUR_API_TOKEN ``` See the [Authentication](/authentication) guide for details. ## Test API keys Use a test API key with the `sk_test_` prefix to validate the full order lifecycle without running Anti-AI, watermark, or AI Detection processing. Test orders can be retained for list, statistics, and detail checks, but they do not produce downloadable result files. See [Test API keys](/test-api-keys) for the upload and state-transition flow. ## Endpoint groups Create and manage Anti-AI image protection orders. Embed invisible digital watermarks into images. Extract and verify watermarks from images. Query, download, and manage orders. Configure webhook endpoints for event notifications. # Get download URL Source: https://docs.bizmori.com/api-reference/orders/download-order api-reference/openapi.yaml GET /api/v2/orders/{orderId}/download Issue a download URL for completed order files. - Only available for orders with `complete` status - URL is valid for 1 hour - Order files are available for download for up to 7 days after order creation - After 7 days, the order transitions to `expired` status and files can no longer be downloaded - Test orders do not create result files; this endpoint returns `404` with `PROCESSED_FILE_NOT_FOUND` for a test order Test orders do not create result files. Calling this endpoint for a test order returns `404 PROCESSED_FILE_NOT_FOUND`. # Get order details Source: https://docs.bizmori.com/api-reference/orders/get-order api-reference/openapi.yaml GET /api/v2/orders/{orderId} With an `sk_test_` key, this endpoint returns only test orders owned by the key's account. Test orders use the same `pending`, `inProgress`, `complete`, and `failed` status values as live orders. # List orders Source: https://docs.bizmori.com/api-reference/orders/list-orders api-reference/openapi.yaml GET /api/v2/orders Retrieve orders with pagination. - Filter by status, type, and channel - Keyword search supported - Sorted by most recent Use the same endpoint with a test API key to list persistent test orders. The response includes only test orders owned by the account associated with the key; live orders are not included. # Recent usage statistics Source: https://docs.bizmori.com/api-reference/orders/recent-stats api-reference/openapi.yaml GET /api/v2/orders/stats/recent Usage statistics for the last 7 days With an `sk_test_` key, the statistics cover the account's test orders only. Test orders do not consume credits or live usage, but their order history can be used to verify list and dashboard reporting flows. # Refresh presigned URLs Source: https://docs.bizmori.com/api-reference/orders/refresh-urls api-reference/openapi.yaml POST /api/v2/orders/{orderId}/refresh-urls Reissue expired upload URLs. Live keys receive S3 presigned URLs; test keys receive absolute `https://api.bizmori.com/api/v2/test-uploads/{signedToken}` URLs. Both are valid for one hour. - Only available for orders with `pending` status - For test orders, the response contains a BIZ MORI test upload URL instead of a storage presigned URL For a test order, this endpoint reissues an upload URL for a file that has not been uploaded yet. The returned URL is a BIZ MORI test upload URL rather than a storage presigned URL. # Create watermark embed order Source: https://docs.bizmori.com/api-reference/watermark-embed/create-order api-reference/openapi.yaml POST /api/v2/orders/wtr-embed Create a watermark embedding order. - Up to 10 watermark texts per image - Up to 100 files per order - Supported image formats (IMG): jpg, jpeg, png, webp, bmp, tiff - Supported document formats (DOCUMENT): pdf ## Input Modes (Upload / URL / Mixed) The input mode is determined by the presence of `originalFileUrl` in each file object: - **Upload mode** (no `originalFileUrl`): Presigned URL issued → Client uploads to S3 → Processing starts automatically - **URL mode** (`originalFileUrl` provided): Image downloaded from the URL and processed immediately - **Mixed mode**: Some files in Upload mode, others in URL mode simultaneously ### Behavior | Condition | Order Status | Confirm Required | |-----------|-------------|-----------------| | All files have `originalFileUrl` | `pending` | No (auto-starts after server downloads) | | Any file without `originalFileUrl` | `pending` | No (auto-starts after upload) | Unlike Anti-AI and AI Detection, Watermark Embed does not require a separate confirm step. Processing starts automatically after file upload completes. With an `sk_test_` key, upload to the returned BIZ MORI test upload URL. The order moves to processing after its uploads finish, without a confirm call. Test processing does not create a downloadable result file. ## Download order results Once your order reaches `complete` status, use the [Get download URL](/api-reference/orders/download-order) endpoint to retrieve a presigned download URL: ```bash theme={null} curl -X GET https://api.bizmori.com/api/v2/orders/{orderId}/download \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` Live order files are available for download for **up to 7 days** after order creation. An `sk_test_` order does not create a result file; its download request returns `404 PROCESSED_FILE_NOT_FOUND`. # Create watermark extract order Source: https://docs.bizmori.com/api-reference/watermark-extract/create-order api-reference/openapi.yaml POST /api/v2/orders/wtr-extract Create a watermark extraction order. - Only 1 file per order - Supports image (jpg, jpeg, png, webp, bmp, tiff) or PDF files - For images, `includeOriginal: true` allows uploading the original file alongside - PDF does not support `includeOriginal` With an `sk_test_` key, upload to the returned BIZ MORI test upload URL. Watermark Extract starts after the upload finishes and uses `_detected` or `_undetected` in the file name to select the simulated result. # Create webhook Source: https://docs.bizmori.com/api-reference/webhooks/create-webhook api-reference/openapi.yaml POST /api/v2/orders/webhooks Register a new webhook. A test API key creates an owned test endpoint when `isTest` is omitted or true; `isTest: false` returns `AUTH_FORBIDDEN`. - Multiple webhooks can be registered - A signing secret is returned upon registration **Important**: The secret is only shown in this response. Store it securely. ## Webhook Event Types | Event Type | Description | |-----------|-------------| | `order.antiAi.completed` | Anti-AI processing completed | | `order.antiAi.failed` | Anti-AI processing failed | | `order.watermarkEmbed.completed` | Watermark embedding completed | | `order.watermarkEmbed.failed` | Watermark embedding failed | | `order.watermarkExtract.completed` | Watermark extraction completed | | `order.watermarkExtract.failed` | Watermark extraction failed | ## Signature Verification Webhook requests include an `X-MoriBiz-Signature` header. The signature is generated using HMAC-SHA256 with the secret issued at registration. ```javascript const crypto = require('crypto'); const signature = crypto.createHmac('sha256', secret) .update(JSON.stringify(payload)) .digest('hex'); ``` ## Retry Policy - Max 3 retries - Exponential backoff (1s, 2s, 4s) - Success response: 2xx status code Set `isTest` to `true` for a test webhook endpoint. Test API keys can manage only test endpoints owned by the same owner. # Delete webhook Source: https://docs.bizmori.com/api-reference/webhooks/delete-webhook api-reference/openapi.yaml DELETE /api/v2/orders/webhooks/{webhookId} Delete a webhook. A test API key can delete only its owner's test endpoint; a live or other-owner ID returns `WEBHOOK_NOT_FOUND` without disclosing existence. # Get webhook details Source: https://docs.bizmori.com/api-reference/webhooks/get-webhook api-reference/openapi.yaml GET /api/v2/orders/webhooks/{webhookId} Retrieve detailed information for a specific webhook. A test API key can retrieve only its owner's test endpoint; a live or other-owner ID returns `WEBHOOK_NOT_FOUND` without disclosing existence. # List webhook events Source: https://docs.bizmori.com/api-reference/webhooks/list-events api-reference/openapi.yaml GET /api/v2/orders/webhooks/{webhookId}/events Retrieve delivery events for a specific webhook with pagination. A test API key sees events only for its owner's test endpoint; a live or other-owner ID returns `WEBHOOK_NOT_FOUND`. - Includes status, event type, and delivery time - Test API keys can manage only test endpoints owned by the same owner # List webhooks Source: https://docs.bizmori.com/api-reference/webhooks/list-webhooks api-reference/openapi.yaml GET /api/v2/orders/webhooks Retrieve registered webhooks with pagination. A test API key sees only endpoints owned by the same owner with `isTest=true`; live keys keep their existing mixed live/test list. - Includes name, URL, active status, and last sent time - `isTest: true` endpoints receive test order events; test API keys can manage only test endpoints owned by the same owner # Retry webhook event Source: https://docs.bizmori.com/api-reference/webhooks/retry-event api-reference/openapi.yaml POST /api/v2/orders/webhooks/{webhookId}/events/{eventId}/retry Manually retry sending a single failed webhook event. A test API key can retry only an event in its owner's test endpoint; an inaccessible endpoint returns `WEBHOOK_NOT_FOUND`, and an event outside an accessible endpoint returns `WEBHOOK_EVENT_NOT_FOUND`. - Only events with `FAILED` status can be retried - On success, the event status changes to `SENT` - If the retry fails, the event remains `FAILED` and a scheduler-based retry is queued for 30 minutes later - Test API keys can manage only test endpoints owned by the same owner # Retry failed webhook events Source: https://docs.bizmori.com/api-reference/webhooks/retry-failed-events api-reference/openapi.yaml POST /api/v2/orders/webhooks/{webhookId}/events/retry-failed Retry all failed webhook events within a specified date range. A test API key can retry only its owner's test endpoint; an inaccessible endpoint returns `WEBHOOK_NOT_FOUND`. - Retries up to 50 failed events per request - Date range must not exceed 7 days - Each event is retried individually - Test API keys can manage only test endpoints owned by the same owner # Update webhook Source: https://docs.bizmori.com/api-reference/webhooks/update-webhook api-reference/openapi.yaml PUT /api/v2/orders/webhooks/{webhookId} Update webhook information. A test API key can update only its owner's test endpoint; omitting `isTest` keeps test mode, true is allowed, and false returns `AUTH_FORBIDDEN`. A live or other-owner ID returns `WEBHOOK_NOT_FOUND`. # Authentication Source: https://docs.bizmori.com/authentication How to authenticate your BIZ MORI API requests All API endpoints require an **API key** in the `Authorization: Bearer` header. Create and store the key in the [BIZ MORI Dashboard](https://app.bizmori.com/keys); it is shown only once. ## Getting your API key 1. Sign in to the [BIZ MORI Dashboard](https://app.bizmori.com) 2. Open [**API Keys**](https://app.bizmori.com/keys) 3. Click **Issue New Key** 4. Copy and securely store the key — it is shown only once Your API key grants access to your account. Keep it out of browser code, repositories, and version control. ## Test API keys Use the automatically issued test API key with the `sk_test_` prefix when you need to verify an integration without running the underlying processing services. BIZ MORI issues one test key per account and organization, and you can retrieve it from the Dashboard API Keys page. Test API keys: * use the same `Authorization: Bearer` authentication as live keys; * use the same order creation, upload, confirm, and query endpoints as live keys; * do not run Anti-AI, watermark, or AI Detection processing; * do not call external processing services or consume credits; * keep test orders separate from live orders; and * do not create downloadable result files. For persistent test orders, the returned upload URL points to a BIZ MORI test upload route. The server consumes the request stream, records upload completion, and discards the file contents. Anti-AI upload mode and AI Detection still require a confirm call; Watermark Embed and Watermark Extract start after their uploads finish. Test orders move through `pending`, `inProgress`, and `complete` or `failed` states. You can list, inspect, and aggregate your test orders with the normal order endpoints. A test download returns `404 PROCESSED_FILE_NOT_FOUND` because no result file is created. A test API key can manage only test webhook endpoints owned by the same owner; see [Test API keys](/test-api-keys) for the complete behavior and examples. ## Using your API key Generate a UUIDv4 for each new logical order. The examples below create one before making the first Anti-AI order. ```bash cURL theme={null} ORDER_IDEMPOTENCY_KEY=$(uuidgen | tr '[:upper:]' '[:lower:]') curl -X POST https://api.bizmori.com/api/v2/orders/anti-ai \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"idempotencyKey\": \"$ORDER_IDEMPOTENCY_KEY\", \"files\": [{\"fileName\": \"image.jpg\"}]}" ``` ```javascript Node.js theme={null} import { randomUUID } from 'node:crypto'; const orderIdempotencyKey = randomUUID(); const response = await fetch('https://api.bizmori.com/api/v2/orders/anti-ai', { method: 'POST', headers: { Authorization: 'Bearer YOUR_API_TOKEN', 'Content-Type': 'application/json' }, body: JSON.stringify({ idempotencyKey: orderIdempotencyKey, files: [{ fileName: 'image.jpg' }] }), }); const data = await response.json(); ``` ```python Python theme={null} import uuid import requests order_idempotency_key = str(uuid.uuid4()) response = requests.post( 'https://api.bizmori.com/api/v2/orders/anti-ai', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, json={'idempotencyKey': order_idempotency_key, 'files': [{'fileName': 'image.jpg'}]}, ) data = response.json() ``` ```php PHP theme={null} true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer YOUR_API_TOKEN', 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode(['idempotencyKey' => $orderIdempotencyKey, 'files' => [['fileName' => 'image.jpg']]])]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` ```ruby Ruby theme={null} require 'securerandom' require 'net/http' require 'json' order_idempotency_key = SecureRandom.uuid uri = URI('https://api.bizmori.com/api/v2/orders/anti-ai') req = Net::HTTP::Post.new(uri, {'Authorization' => 'Bearer YOUR_API_TOKEN', 'Content-Type' => 'application/json'}) req.body = JSON.generate(idempotencyKey: order_idempotency_key, files: [{fileName: 'image.jpg'}]) data = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) {|http| http.request(req) }.body) ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "net/http" "github.com/google/uuid" ) func main() { orderIdempotencyKey := uuid.NewString() body, _ := json.Marshal(map[string]any{"idempotencyKey": orderIdempotencyKey, "files": []map[string]string{{"fileName": "image.jpg"}}}) req, _ := http.NewRequest("POST", "https://api.bizmori.com/api/v2/orders/anti-ai", bytes.NewBuffer(body)) req.Header.Set("Authorization", "Bearer YOUR_API_TOKEN") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ```java Java theme={null} import java.net.URI; import java.net.http.*; import java.util.UUID; public class Main { public static void main(String[] args) throws Exception { String orderIdempotencyKey = UUID.randomUUID().toString(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.bizmori.com/api/v2/orders/anti-ai")) .header("Authorization", "Bearer YOUR_API_TOKEN") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString( "{\"idempotencyKey\":\"" + orderIdempotencyKey + "\",\"files\":[{\"fileName\":\"image.jpg\"}]}" )) .build(); HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); } } ``` ## Idempotency Use a new UUIDv4 for every new logical order. Anti-AI and AI Detection confirms are separate logical requests, so generate a different UUIDv4 for each confirm. Reuse the previous value only when retrying the same logical request after a network failure. Sending the same key again returns the original order instead of creating a new one. ## Rate limits All requests are subject to a global limit of 1,000 requests per 15 minutes and include standard `RateLimit` headers. Some endpoints may apply additional endpoint-specific limits. For live order API keys, plan usage quota is separate from the global request limit. When that quota is exhausted and no active credit pack is available, the order API returns HTTP 429 with `{ "code": "PLAN_LIMIT_EXCEEDED" }`. Test API keys bypass the plan usage quota, but not the global request limiter, which runs before API-key authentication. ## Live and test API keys With a live key, `uploadUrl` is an S3 presigned URL. With a test key, it is an absolute BIZ MORI API URL under `https://api.bizmori.com/api/v2/test-uploads/{signedToken}`. Both expire one hour after issuance; see [Quickstart](/quickstart) for upload and refresh behavior, and [Webhooks](/webhooks) for test-key webhook scope. # Error Codes Source: https://docs.bizmori.com/errors Complete reference of BIZ MORI API error codes All error responses follow this format: ```json theme={null} { "code": "ERROR_CODE" } ``` The HTTP status code conveys the error category. Use the `code` field for programmatic error handling and i18n/localization. ## Authentication errors | Error Code | HTTP Status | Description | | --------------------------- | ----------- | -------------------------------- | | `AUTH_NOT_AUTHENTICATED` | 401 | Authentication required | | `AUTH_TOKEN_EXPIRED` | 401 | Authentication token has expired | | `AUTH_TOKEN_INVALID` | 401 | Invalid authentication token | | `AUTH_TOKEN_INVALID_FORMAT` | 401 | Token format is invalid | | `AUTH_FORBIDDEN` | 403 | Insufficient permissions | ## Validation errors | Error Code | HTTP Status | Description | | ------------------------------------ | ----------- | -------------------------- | | `VALIDATION_FAILED` | 400 | Input validation failed | | `VALIDATION_IDEMPOTENCY_KEY_INVALID` | 400 | Invalid idempotency key | | `INVALID_PARAMETER` | 400 | Invalid parameter provided | ## Order errors | Error Code | HTTP Status | Description | | -------------------------- | ----------- | ---------------------------------------------- | | `ORDER_NOT_FOUND` | 404 | Order not found | | `ORDER_NOT_PENDING` | 400 | Order is not in pending status | | `ORDER_NOT_OWNER` | 403 | Not the order owner | | `ORDER_EXPIRED` | 400 | Order has expired | | `ORDER_NOT_COMPLETED` | 400 | Order is not completed | | `ORDER_FILE_NOT_UPLOADED` | 400 | Order file has not been uploaded | | `ORDER_ALREADY_CONFIRMED` | 400 | Order has already been confirmed | | `ORDER_ALREADY_COMPLETED` | 400 | Order is already completed | | `ORDER_ALREADY_FAILED` | 400 | Order has already failed | | `ORDER_TYPE_NOT_MATCH` | 400 | Order type does not match | | `ORDER_TYPE_CANT_DOWNLOAD` | 400 | This order type cannot be downloaded | | `PROCESSED_FILE_NOT_FOUND` | 404 | No processed result file exists for this order | | `ORDER_TIMEOUT` | 408 | Order processing timed out | ## Test upload errors | Error Code | HTTP Status | Description | | ----------------------- | ----------- | ------------------------------------------------ | | `TEST_UPLOAD_TOO_LARGE` | 413 | The test upload exceeds the 50 MiB default limit | ## Usage & plan errors | Error Code | HTTP Status | Description | | ---------------------- | ----------- | ------------------------ | | `USAGE_LIMIT_EXCEEDED` | 400 | Usage limit exceeded | | `USAGE_NOT_ENOUGH` | 400 | Insufficient usage quota | | `PLAN_NOT_FOUND` | 404 | Plan not found | ## Webhook errors | Error Code | HTTP Status | Description | | ------------------- | ----------- | ----------------- | | `WEBHOOK_NOT_FOUND` | 404 | Webhook not found | ## Rate limiting | Error Code | HTTP Status | Description | | --------------------- | ----------- | ----------------- | | `RATE_LIMIT_EXCEEDED` | 429 | Too many requests | ## Server errors | Error Code | HTTP Status | Description | | ----------------------- | ----------- | --------------------- | | `SERVER_INTERNAL_ERROR` | 500 | Internal server error | # Introduction Source: https://docs.bizmori.com/index BIZ MORI API — Programmatic digital content protection for images The **BIZ MORI API** gives you programmatic access to digital content protection services. Protect images from unauthorized AI training, embed invisible ownership watermarks, and verify watermark authenticity — all via a simple REST API. ## Services Apply invisible protection that prevents AI systems from using your images for model training — without visible alteration. Embed imperceptible digital watermarks to track ownership and prove image authenticity. Detect and read embedded watermarks to verify image origin and ownership. Determine whether an image was generated by AI. Supports a wide range of generative models. ## Get started Obtain an API key and learn how to authenticate every request. Make your first API call in minutes with a step-by-step walkthrough. Full reference of error codes, HTTP statuses, and troubleshooting guidance. Receive push notifications when order processing completes or fails. ## Base URL ``` https://api.bizmori.com ``` All endpoints are served over **HTTPS**. HTTP requests are not supported. ## Request format Send requests with `Content-Type: application/json` and include your API key in the `Authorization` header: ``` Authorization: Bearer YOUR_API_TOKEN ``` ## Response format All responses return JSON. The shape depends on whether the request succeeded. **Success** ```json theme={null} { "data": { ... } } ``` **Error** ```json theme={null} { "code": "ERROR_CODE" } ``` Use the HTTP status code to identify the error category, and the `code` field for programmatic handling and localization. See [Error Codes](/errors) for the full list. # Acceptable Use Policy (AUP) Source: https://docs.bizmori.com/legal/aup Permitted and prohibited uses of the BIZ MORI service # BizMORI Acceptable Use Policy (AUP) **Effective Date:** November 1, 2025 **Last Updated:** November 1, 2025 *** This Acceptable Use Policy ("AUP") is part of the BizMORI Terms of Service between **MORI SOLUTION, Inc.** ("MORI") and Customer. *** ## 1. General Principles BizMORI protects images from unauthorized AI training and usage through digital watermarking. Use the Service lawfully, ethically, and in accordance with this policy. *** ## 2. Permitted Uses * Embedding watermarks into images you own or are authorized to process * Detecting watermarks for verification * Integrating watermark protection into your products/platforms via the API * Protecting copyrighted visual content from unauthorized AI training *** ## 3. Prohibited Uses ### 3.1 Illegal Activities * Processing images in violation of applicable law * Facilitating copyright infringement, identity theft, or fraud * Processing CSAM (reported to authorities immediately) * Spamming, phishing, or unsolicited bulk communications * Content violating Korea's Information and Communications Network Act (정보통신망법) ### 3.2 Unauthorized Content * Processing images you don't own or lack rights to modify * Removing, altering, or circumventing third-party watermarks * Violating DMCA (17 U.S.C. §1201-1202) or Korea Copyright Act (저작권법 제103조) ### 3.3 Service Abuse * Circumventing rate limits, quotas, or access controls * Unauthorized security testing * Creating multiple free accounts to bypass limits * Reverse engineering watermarking algorithms * Exposing API Keys in client-side code or public repositories ### 3.4 AI Training and Competitive Use * Using the Service or outputs to train AI models * Benchmarking against competitors without written consent * Systematic scraping of Service outputs beyond normal business use ### 3.5 Harmful Content * Uploading malware or content designed to degrade the Service * Submitting files intended to exhaust system resources ### 3.6 Unauthorized Resale * Reselling or white-labeling the Service without written consent *** ## 4. Content Standards MORI does not actively monitor uploaded content but reserves the right to act on violations. CSAM results in immediate termination and reporting to NCMEC. Content violating trade sanctions, EU Digital Services Act, or Korean law is prohibited. DSA notices: [legal@bizmori.com](mailto:legal@bizmori.com). *** ## 5. Rate Limits Rate limits apply per your Plan. Even on Unlimited plans, MORI may throttle requests degrading service quality for others. *** ## 6. Enforcement 1. **Warning** — written notice with cure request 2. **Throttling** — temporary rate limit reduction 3. **Suspension** — pending investigation 4. **Termination** — for severe or repeated violations Non-critical violations: 5 business days to cure. Critical violations (illegal content, security threats): immediate suspension. Appeals: [support@bizmori.com](mailto:support@bizmori.com) within 10 business days. **Vulnerability Disclosure:** Report security issues to [security@bizmori.com](mailto:security@bizmori.com). No legal action for good-faith reports. *** ## 7. Changes Material changes: 30 days' notice. Non-material: 15 days. Continued use constitutes acceptance. *** ## Contact **MORI SOLUTION, Inc.** Website: [https://mori-corp.io](https://mori-corp.io) **General Inquiries:** [support@bizmori.com](mailto:support@bizmori.com) **Legal Notices:** [legal@bizmori.com](mailto:legal@bizmori.com) **Privacy Requests:** [privacy@bizmori.com](mailto:privacy@bizmori.com) **Security Reports:** [security@bizmori.com](mailto:security@bizmori.com) **Developer Portal:** [https://app.bizmori.com](https://app.bizmori.com) **API Documentation:** [https://docs.bizmori.com](https://docs.bizmori.com) *** *This AUP is part of the BizMORI Terms of Service.* # Privacy Policy Source: https://docs.bizmori.com/legal/privacy How BIZ MORI collects, uses, and protects your data # BizMORI Privacy Policy **Effective Date:** November 1, 2025 **Last Updated:** November 1, 2025 *** ## 1. Introduction This Privacy Policy describes how **MORI SOLUTION, Inc.** ("MORI", "we") and its affiliate **MORI Corp.** (주식회사 모리) collect, use, share, and protect Personal Data in connection with the BizMORI service ("Service"). MORI SOLUTION, Inc. is the data controller (GDPR). MORI Corp. operates the Service infrastructure in South Korea and processes data on behalf of MORI SOLUTION, Inc. This policy applies to visitors of mori-corp.io, BizMORI API users, and business customer representatives. *** ## 2. Information We Collect **Account Information:** Name, email, company name, billing address, payment information (processed by our payment provider). Credentials are securely hashed. **Service Usage Data:** API request logs (endpoint, timestamp, response code), IP address, user agent, rate limit usage. **Customer Data (Processed on Your Behalf):** Images uploaded for watermark processing and output files. We process Customer Data solely to provide the Service. We do not train AI models on Customer Data. **Communication Data:** Support requests, feedback, and survey responses. **Automatically Collected Data:** Browser type, OS, device info (web dashboard), cookies (see Section 7). **Note on Biometric Data:** Uploaded images may incidentally contain biometric data (e.g., facial images). MORI does not extract or analyze biometric identifiers; processing is solely for watermark embedding and detection. **Customers are responsible for ensuring compliance with applicable biometric data laws (e.g., Illinois BIPA, GDPR Art. 9, Korea PIPA Art. 23) before uploading images containing biometric identifiers.** *** ## 3. How We Use Your Information | Purpose | Legal Basis (GDPR) | | ------------------------------------- | ----------------------------------- | | Provide and operate the Service | Contract performance (Art. 6(1)(b)) | | Process payments and billing | Contract performance (Art. 6(1)(b)) | | Service notifications and support | Contract performance (Art. 6(1)(b)) | | Security and fraud prevention | Legitimate interest (Art. 6(1)(f)) | | Service improvement (aggregated data) | Legitimate interest (Art. 6(1)(f)) | | Legal compliance | Legal obligation (Art. 6(1)(c)) | | Marketing (with consent) | Consent (Art. 6(1)(a)) | We do not use Personal Data for automated decision-making that produces legal effects (GDPR Art. 22). *** ## 4. Data Retention | Data Type | Retention | | -------------------------- | ---------------------------------- | | Processing temporary files | Deleted immediately | | Completed order files | 30 days | | API request logs | 90 days | | Account information | Account duration + 30 days | | Billing records | Up to 5 years (as required by law) | Data is securely deleted or anonymized upon expiration using cryptographic erasure or overwrite methods. *** ## 5. How We Share Your Information We do not sell Personal Data. We share data only as follows: * **Sub-processors:** AWS (cloud infrastructure, South Korea), Clerk (authentication, US), Sentry (error monitoring, US), Stripe (payments, US). Updated with 30 days' notice. * **Affiliates:** MORI Corp. (South Korea) for service operations. * **Legal Requirements:** When required by law or governmental request. * **Business Transfers:** In connection with mergers, acquisitions, or asset sales. *** ## 6. International Data Transfers Data may be transferred to South Korea (MORI Corp., AWS Seoul) and the United States (service providers, MORI SOLUTION, Inc.). For EEA/UK transfers: Standard Contractual Clauses (EU 2021/914) and adequacy decisions where applicable (South Korea has EU adequacy since December 2021). UK transfers also use the ICO's International Data Transfer Addendum. For South Korea transfers: PIPA Article 28-8 compliance with contractual safeguards. *** ## 7. Cookies We use essential cookies (authentication, security) without consent and functional/analytics cookies with consent per the EU ePrivacy Directive. Manage preferences via the cookie banner or dashboard footer settings. *** ## 8. Your Rights ### GDPR (EEA/UK Residents) Access, rectification, erasure, restriction, portability, objection, consent withdrawal, and right to lodge a complaint with your supervisory authority. Contact: [privacy@bizmori.com](mailto:privacy@bizmori.com). Response within 30 days. ### CCPA/CPRA (California Residents) Right to know, delete, correct, and opt-out of sale/sharing. **We do not sell or share personal information.** Categories collected: identifiers, commercial info, internet activity, professional info. Contact: [privacy@bizmori.com](mailto:privacy@bizmori.com). Response within 45 days. ### Other U.S. State Laws Residents of Virginia, Colorado, Connecticut, Utah, and other states with privacy laws may have similar rights. Contact: [privacy@bizmori.com](mailto:privacy@bizmori.com). ### Korea PIPA (한국 거주자) 열람권(제35조), 정정·삭제권(제36조), 처리정지권(제37조), 동의 철회권. **개인정보처리자:** MORI Corp. (주식회사 모리) **개인정보 보호책임자(CPO):** Kyuseok Kim, CEO, MORI Corp. — [privacy@bizmori.com](mailto:privacy@bizmori.com) 개인정보 전송 요구권(제35조의2) 보장. 열람 청구 시 10일 이내 조치. **피해구제:** 개인정보분쟁조정위원회(1833-6972), 개인정보침해신고센터(118), 대검찰청(1301), 경찰청(182). **개인정보 처리 위탁:** AWS (인프라), Clerk (인증), Sentry (오류 모니터링), Stripe (결제). 변경 시 30일 전 통지. *** ## 9. Data Security We implement encryption at rest and in transit (TLS 1.2+), secure credential hashing, WAF, rate limiting, RBAC, monitoring, and input validation. *** ## 10. Security Incidents In the event of a data breach likely to cause high risk, we will notify affected individuals without undue delay per GDPR Article 34 and PIPA Article 34, and cooperate with authorities. *** ## 11. Children's Privacy The Service is B2B and not directed at individuals under 16 (or 13 under COPPA). We do not knowingly collect children's data. *** ## 12. Changes Material changes will be posted at docs.bizmori.com/legal/privacy with 30 days' email notice. *** ## 13. Contact **MORI SOLUTION, Inc.** Website: [https://mori-corp.io](https://mori-corp.io) **General Inquiries:** [support@bizmori.com](mailto:support@bizmori.com) **Legal Notices:** [legal@bizmori.com](mailto:legal@bizmori.com) **Privacy Requests:** [privacy@bizmori.com](mailto:privacy@bizmori.com) **Security Reports:** [security@bizmori.com](mailto:security@bizmori.com) **Developer Portal:** [https://app.bizmori.com](https://app.bizmori.com) **API Documentation:** [https://docs.bizmori.com](https://docs.bizmori.com) *** *Available in English and Korean. English prevails for international customers; Korean prevails for South Korea customers.* # Terms of Service Source: https://docs.bizmori.com/legal/terms Terms governing your use of the BIZ MORI API # BizMORI Terms of Service **Effective Date:** November 1, 2025 **Last Updated:** November 1, 2025 *** ## 1. Introduction These Terms of Service ("Terms") are a binding agreement between you ("Customer") and **MORI SOLUTION, Inc.** ("MORI", "we"). The BizMORI service is technically operated by **MORI Corp.** (주식회사 모리), a Korean affiliate of MORI SOLUTION, Inc. **Service.** BizMORI is an AI image protection solution providing digital watermark embedding and detection via a B2B SaaS API ("Service"), accessible at api.bizmori.com. By using the Service, you agree to these Terms. If acting on behalf of an organization, you represent authority to bind it. *** ## 2. Account and Access 2.1. You must create an account with accurate information and maintain the confidentiality of your credentials and API Keys. 2.2. API Keys are Confidential Information. Do not share, publish, or embed them in client-side code. Compromised keys must be revoked immediately via your dashboard. 2.3. You are responsible for all activity under your account. *** ## 3. Use of the Service 3.1. **License.** MORI grants you a limited, non-exclusive, non-transferable, revocable license to use the Service via the API per your Plan and Documentation. 3.2. **Acceptable Use.** Use is subject to the Acceptable Use Policy (AUP) at docs.bizmori.com/legal/aup. Violations may result in suspension or termination. 3.3. **Rate Limits.** API usage is subject to rate limits per your Plan as published at app.bizmori.com/pricing and docs.bizmori.com. Limits may change with reasonable notice. 3.4. **Restrictions.** You shall not: (a) reverse engineer the Service; (b) build a competing product; (c) circumvent rate limits or security measures; (d) sublicense or resell access without written consent; (e) use the Service in violation of applicable law. 3.4A. **Customer Data Compliance.** You warrant that Customer Data complies with applicable law, including biometric data protection laws (e.g., Illinois BIPA, GDPR Art. 9, Korea PIPA Art. 23). If uploaded images contain biometric identifiers (e.g., facial images), you are responsible for obtaining all required consents. 3.5. **DMCA.** Watermarks embedded by the Service may constitute copyright management information under 17 U.S.C. §1202. Unauthorized removal or circumvention may violate the DMCA. 3.6. **Intermediary Status.** MORI acts as a neutral technology platform and does not monitor or edit Customer Data (47 U.S.C. §230). 3.7. **EU AI Act.** The Service is classified as a minimal-risk AI system under EU Regulation 2024/1689. MORI provides transparency as required by Article 50. 3.8. **Digital Services Act.** Where applicable, MORI complies with the EU Digital Services Act (Regulation (EU) 2022/2065). *** ## 4. Customer Data 4.1. **Ownership.** You retain all rights in your Customer Data and Processed Output. MORI claims no ownership. 4.2. **License to MORI.** You grant MORI a limited license to process Customer Data solely to provide the Service. This license terminates upon data deletion. 4.3. **Data Retention.** Processing temporary files are deleted immediately. Completed order files are retained for 30 days. API logs for 90 days. Account information for account duration plus 30 days. Billing records as required by law (up to 5 years). Full schedule available at docs.bizmori.com/legal/privacy. 4.4. **No AI Training.** MORI will not use Customer Data to train or develop any AI models. 4.5. **Government Access.** MORI may disclose Customer Data pursuant to lawful requests under the CLOUD Act or similar laws. Where legally permitted, MORI will notify Customer prior to disclosure. *** ## 5. Fees and Payment 5.1. Fees are based on your Plan at app.bizmori.com/pricing. MORI may update pricing with 30 days' notice. 5.2. Subscriptions are billed in advance (monthly or annually) by MORI SOLUTION, Inc. Usage-based charges are billed in arrears. 5.3. Fees exclude taxes. You are responsible for applicable taxes except taxes on MORI's net income. 5.4. Overdue amounts accrue interest at 1.5% per month or the legal maximum. MORI may suspend access after 15 days of non-payment following notice. 5.5. Prepaid fees are non-refundable except as required by law. 5.6. **Auto-Renewal.** MORI provides clear disclosure of auto-renewal terms before purchase and a reminder at least 30 days before annual renewal. Cancel anytime via your dashboard. 5.7. **Korean Consumer Protection.** Korean consumers under the Act on Consumer Protection in Electronic Commerce may exercise withdrawal rights within 7 days of subscription, subject to statutory exceptions (Article 17). In particular, withdrawal is not available once the provision of the digital content/service has commenced (i.e., API calls have been made), provided the customer was clearly informed of this limitation before purchase (Article 17(2)(5)). *** ## 6. Service Levels MORI targets commercially reasonable availability for all plans. Formal Service Level Agreement (SLA) terms, including uptime commitments and service credits, are available for Enterprise customers upon request at [legal@bizmori.com](mailto:legal@bizmori.com). Where an SLA has been executed, it is incorporated by reference. *** ## 7. Intellectual Property 7.1. The Service, API, algorithms, and related technology are MORI's exclusive property. No rights are granted except the license in Section 3.1. 7.2. Feedback you provide may be used by MORI without obligation. *** ## 8. Confidentiality 8.1. Each party shall protect the other's Confidential Information and not disclose it except as needed to perform under these Terms. 8.2. Exceptions: publicly available information, previously known information, independently developed information, or legally required disclosures. 8.3. Confidentiality survives termination for three years; trade secrets are protected indefinitely. *** ## 9. Warranties and Disclaimer 9.1. Each party warrants authority to enter these Terms. 9.2. MORI warrants the Service will perform materially per Documentation and be provided professionally with commercially reasonable security. 9.3. Customer warrants it has rights to Customer Data and will comply with applicable law. 9.4. **EXCEPT AS STATED ABOVE, THE SERVICE IS PROVIDED "AS IS." MORI DISCLAIMS ALL OTHER WARRANTIES, INCLUDING MERCHANTABILITY, FITNESS FOR PURPOSE, AND NON-INFRINGEMENT.** 9.5. **Watermarking Accuracy.** Watermark embedding and detection involve probabilistic processes. MORI does not guarantee: (a) watermarks will survive all possible image transformations; (b) zero false positive or false negative detection rates; (c) watermarks will be imperceptible in all circumstances. Detection accuracy depends on image modifications, compression levels, and format conversions applied after embedding. *** ## 10. Limitation of Liability 10.1. **MORI'S TOTAL LIABILITY SHALL NOT EXCEED THE GREATER OF: (A) AMOUNTS PAID IN THE 12 MONTHS PRECEDING THE CLAIM; OR (B) USD \$100.** 10.2. **NEITHER PARTY SHALL BE LIABLE FOR INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES.** 10.3. These limitations do not apply to: (a) breach of restrictions or AUP; (b) indemnification obligations; (c) breach of confidentiality; (d) willful misconduct or gross negligence; (e) liability that cannot be limited by law. 10.4. **Mandatory Consumer Protection.** In jurisdictions with mandatory consumer protection laws (including Korea and the EU), these limitations do not restrict rights under such laws. *** ## 11. Indemnification 11.1. MORI will defend Customer against third-party IP infringement claims related to the Service. 11.2. Customer will defend MORI against claims arising from Customer Data, misuse, or Customer's products incorporating the Service. *** ## 12. Term and Termination 12.1. These Terms continue until terminated. 12.2. Either party may terminate with 30 days' notice. Customer may terminate by ceasing use and closing their account. 12.3. Either party may terminate immediately if the other materially breaches and fails to cure within 30 days, or becomes insolvent. 12.4. Upon termination, licenses cease, Customer deletes API Keys, and MORI deletes data per the retention schedule. Sections 4.1, 7–11, 13–15 survive. 12.5. Customer may request data export within 30 days of termination. *** ## 13. Export Control and Sanctions Each party shall comply with applicable export control and sanctions laws. Customer represents it is not in a sanctioned country or on any restricted party list, and will not use the Service for weapons development. *** ## 14. Dispute Resolution 14.1. **Governing Law.** Delaware, United States. 14.2. **Informal Resolution.** Parties shall attempt good-faith negotiation for 30 days before formal proceedings. 14.3. **Arbitration.** Unresolved disputes shall be settled by ICC arbitration in Singapore, in English, by a single arbitrator. 14.4. Either party may seek injunctive relief in any court to protect IP or Confidential Information. 14.5. **CLASS ACTION WAIVER.** Each party waives rights to class actions or consolidated proceedings. 14.6. **EU Customers** retain the right to bring claims in their country of domicile per mandatory EU law. The EU ODR platform is available at [https://ec.europa.eu/consumers/odr](https://ec.europa.eu/consumers/odr). 14.7. **Korean Consumers** may bring claims before Korean courts to the extent Singapore arbitration is prohibited by Korean consumer protection law. *** ## 15. General 15.1. **Force Majeure.** Neither party is liable for delays beyond reasonable control. If lasting 60+ days, either may terminate. 15.2. **Assignment.** No assignment without consent, except in mergers/acquisitions. 15.3. **Entire Agreement.** These Terms, Privacy Policy, DPA (available upon request at [legal@bizmori.com](mailto:legal@bizmori.com)), SLA (where applicable), and AUP constitute the entire agreement. 15.4. **Amendments.** MORI may update Terms with 30 days' notice. Materially adverse changes give Customer 30 days to terminate without penalty. 15.5. **Severability.** Unenforceable provisions are modified minimally; remaining provisions continue. 15.6. **Notices.** To MORI: [legal@bizmori.com](mailto:legal@bizmori.com). To Customer: email on file. *** ## 16. Contact **MORI SOLUTION, Inc.** Website: [https://mori-corp.io](https://mori-corp.io) **General Inquiries:** [support@bizmori.com](mailto:support@bizmori.com) **Legal Notices:** [legal@bizmori.com](mailto:legal@bizmori.com) **Privacy Requests:** [privacy@bizmori.com](mailto:privacy@bizmori.com) **Security Reports:** [security@bizmori.com](mailto:security@bizmori.com) **Developer Portal:** [https://app.bizmori.com](https://app.bizmori.com) **API Documentation:** [https://docs.bizmori.com](https://docs.bizmori.com) *** *By using BizMORI, you agree to these Terms of Service.* # MCP Connector Source: https://docs.bizmori.com/mcp-connector Use BIZ MORI from Claude by chatting — no code, no API key The **BIZ MORI MCP connector** lets you use BIZ MORI directly inside an AI assistant like Claude. You connect once with your BIZ MORI account, then ask in plain language: > "Check whether this image is AI-generated: ``" Claude runs the job on BIZ MORI and reports the result back in the chat. No code, no API key, no dashboard. **MCP** (Model Context Protocol) is an open standard that lets AI assistants connect to outside services. You do not need to understand it to use this. ## What you can do Ask whether an image was generated by AI. You get a probability, a verdict, and heatmap images. Protect images from being used as AI training data. Up to 100 images at once. Add an invisible ownership watermark carrying your text. Check whether a file carries a BIZ MORI watermark and read its text. "Show me my jobs from this week" — list past jobs and check their results. Run a job under your personal account or one of your organizations. ## Connect the connector In the Claude app (web or desktop), go to **Settings → Connectors → Add custom connector**. ``` https://mcp.bizmori.com/mcp ``` Give it a name you will recognize, such as `BIZ MORI`. A BIZ MORI login window opens. Sign in with the same account you use for [app.bizmori.com](https://app.bizmori.com) and approve the connection. You do **not** need an API key. Start a new chat and try: ```text theme={null} Use BIZ MORI to check if this image is AI-generated: ``` ## Image upload methods **Which upload methods you can use depends on the tool you are using.** | | Image attached in chat | File on your computer | Image URL | | -------------------------------- | :--------------------: | :-------------------: | :-------: | | **Claude app** (web / desktop) | X | X | ✓ | | **Claude Code** (terminal / IDE) | — | ✓ | ✓ | | **Codex** | — | ✓ | ✓ | An image you **attach to the chat cannot be used**. The attachment goes to Claude's eyes only; the connector never receives the actual file. This is a platform limitation, not something BIZ MORI can fix. The app also has no access to your computer's files. So upload the image somewhere that gives you a link — Google Drive, Dropbox, or Notion with link sharing turned on — and paste that link into the chat. Attaching a photo and asking for Anti-AI protection will not work. Claude will ask you for a link instead. Claude Code can read your files, so just tell it where the image is: ```text theme={null} Apply Anti-AI protection to ~/Pictures/artwork.png ``` It uploads the file to BIZ MORI for you. An image URL works too. Same as Claude Code: give it the file path and it handles the upload. ## Getting your results Claude waits for the job to finish and gives you the result in the chat. | Service | What you get back | | --------------------- | ----------------------------------------------------------------------------- | | **AI Detection** | A probability and verdict, plus a heatmap, an overlay, and a thumbnail image. | | **Watermark Extract** | The detected watermark text. | | **Anti-AI** | A download link for the protected image. Several images come back as one ZIP. | | **Watermark Embed** | A download link for the result ZIP. | Every result also includes a link to open that job on your [dashboard](https://app.bizmori.com), and the name of the account it was billed to. AI Detection produces all three images by default. They do not cost extra — one check counts as one job no matter how many images come back. **A long job may come back as "still processing."** Claude only waits about two minutes — a large Anti-AI batch or a heavy watermark job can take longer. This is not a failure; the job keeps running. Get the result either way: * Download it from the [dashboard](https://app.bizmori.com), where all your completed jobs are listed. * Or come back to the chat a few minutes later and ask Claude to check that job again. Download links expire after a while. If a link has stopped working, ask Claude for a new link for that job, or download from the dashboard. ## Good things to ask Put your image link in ``. If you use Claude Code or Codex CLI, a file path works too. ```text theme={null} Check if this image is AI-generated: ``` ```text theme={null} Apply Anti-AI protection at high strength to all the images in this ZIP: ``` ```text theme={null} Embed the watermark "© 2026 MORI Corp" into this image: ``` ```text theme={null} Does this file have a BIZ MORI watermark in it? ``` ```text theme={null} Show me my BIZ MORI jobs from the last week and which ones failed. ``` ## Billing account If you belong to one or more organizations on BIZ MORI, jobs can be billed to your personal account **or** to an organization. The connector never guesses — it will stop and ask you which one to use. Answer with the account name and the job runs. **You are only asked once** — your choice is remembered, so later jobs use the same account without asking again. Name a different account any time to switch. Every result tells you which account it was billed to. You can also say it upfront: ```text theme={null} Apply Anti-AI protection to these images, billed to MORI Corp: ``` If your personal account is the only one you have, nothing is asked and it is used automatically. ## Troubleshooting That is the platform limitation described above, not a bug. Put the image somewhere with a shareable link and paste the link instead. The job ran longer than Claude waits, which happens with large batches. It is still running, not failed. Download the result from the [dashboard](https://app.bizmori.com), or ask Claude to check that job again in a few minutes. Disconnect and reconnect the connector in **Settings → Connectors**. Claude caches the tool list, so newly added capabilities only appear after reconnecting. Until then it may split a batch into many separate jobs or attempt browser-based workarounds. You may be running jobs under your personal free account instead of your organization. Ask Claude which accounts are available, then name the organization when you start a job. The link expired. Ask Claude to look up that job again for a new link, or download it from the dashboard. Contact [support@bizmori.com](mailto:support@bizmori.com). ## Prefer to write code? The connector covers everyday use. For building BIZ MORI into your own product, use the REST API directly. Make your first API call step by step. Hand a coding agent the full integration contract in one paste. # Quickstart Source: https://docs.bizmori.com/quickstart Get started with BIZ MORI API — choose a service to begin Choose a quickstart guide for the service you want to integrate: Protect images from AI training and generation. Embed invisible watermarks into images. Detect and extract watermarks from images. Detect AI-generated images with probability scores. ## Prerequisites All quickstart guides share these requirements: * A BIZ MORI API key ([get one here](https://app.bizmori.com/keys)) * An image file in a supported format (varies by service) Use the automatically issued [`sk_test_` key](/test-api-keys) to exercise the test lifecycle. A live key receives an S3 presigned `uploadUrl`; a test key receives `https://api.bizmori.com/api/v2/test-uploads/{signedToken}`. Both are valid for one hour. PUT the binary body with `Content-Type` and no Authorization header, then use [Refresh URLs](/api-reference/orders/refresh-urls) if it expires. Test uploads are streamed only: BIZ MORI does not store them, consume usage, or run S3/GPU processing. Use a UUIDv4 idempotency key for every new logical request; reuse it only for that request's network retry. ## Not a developer? Connect BIZ MORI to Claude and run jobs by chatting — no code and no API key required. ## Building with an AI coding agent? Copy one prompt into Claude Code, Cursor, or Codex and hand your agent the full integration contract. # AI Detection Source: https://docs.bizmori.com/quickstart/ai-detection Detect AI-generated images with probability scoring This guide walks you through detecting whether an image is AI-generated — from uploading an image to reading the detection result. Every endpoint here is plain HTTPS and JSON, so the same calls work from a server or straight from the browser. Each step includes a **React** tab, and a [full copy-paste component](#full-react-example) is at the bottom of the page. ## Prerequisites * A BIZ MORI API key ([get one here](https://app.bizmori.com/keys)) * An image file to analyze (`jpeg`, `jpg`, `png`, `webp`, `bmp`, or `tiff`) Use the automatically issued [`sk_test_` key](/test-api-keys) to exercise the order lifecycle without running AI Detection or consuming credits. Test uploads discard file contents and return deterministic probability values. Use a live API key when you need an actual detection result. ## Step 1: Create an order Create an AI Detection order with your image file name. ```bash cURL theme={null} ORDER_IDEMPOTENCY_KEY=$(uuidgen | tr '[:upper:]' '[:lower:]') curl -X POST https://api.bizmori.com/api/v2/orders/ai-detection \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "idempotencyKey": "'$ORDER_IDEMPOTENCY_KEY'", "fileName": "photo.jpg" }' ``` ```javascript Node.js theme={null} import { randomUUID } from 'node:crypto'; const orderIdempotencyKey = randomUUID(); const response = await fetch('https://api.bizmori.com/api/v2/orders/ai-detection', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_TOKEN', 'Content-Type': 'application/json', }, body: JSON.stringify({ idempotencyKey: orderIdempotencyKey, fileName: 'photo.jpg', }), }); const { data } = await response.json(); ``` ```jsx React theme={null} // One helper for every JSON call. The API returns `{ code: "ERROR_CODE" }` // on failure, so surface that code instead of a bare status number. const API_BASE = 'https://api.bizmori.com/api/v2'; const API_TOKEN = import.meta.env.VITE_MORI_API_TOKEN; async function api(path, { method = 'GET', body, idempotencyKey } = {}) { const res = await fetch(`${API_BASE}${path}`, { method, headers: { Authorization: `Bearer ${API_TOKEN}`, ...(body && { 'Content-Type': 'application/json' }), }, body: body && JSON.stringify({ idempotencyKey, ...body }), }); if (!res.ok) { const { code } = await res.json().catch(() => ({})); throw new Error(code ?? `HTTP_${res.status}`); } return (await res.json()).data; } // `file` is the single File the user picked const order = await api('/orders/ai-detection', { method: 'POST', idempotencyKey: crypto.randomUUID(), body: { fileName: file.name }, }); ``` ```python Python theme={null} import uuid import requests order_idempotency_key = str(uuid.uuid4()) res = requests.post( 'https://api.bizmori.com/api/v2/orders/ai-detection', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, json={ 'idempotencyKey': order_idempotency_key, 'fileName': 'photo.jpg', }, ) data = res.json()['data'] ``` **Response:** ```json theme={null} { "data": { "orderId": "123456789", "orderName": "ai_detection_2026-03-18", "status": "pending", "file": { "fileId": 1, "fileName": "photo.jpg", "uploadUrl": "https://s3.amazonaws.com/...", "fileKey": "ai-detection/123456789/1/photo.jpg" } } } ``` You can optionally request heatmap and overlay images by adding `"options": { "generateHeatmap": true, "generateOverlay": true }` to the request body. These will be available in the order detail once processing completes. ## Step 2: Upload the file PUT your file to the presigned `uploadUrl` from Step 1. This is a direct S3 upload — **no Authorization header needed**. ```bash cURL theme={null} curl -X PUT "https://s3.amazonaws.com/..." \ -H "Content-Type: image/jpeg" \ --data-binary @photo.jpg ``` ```jsx React theme={null} // fetch() cannot report upload progress, so use XMLHttpRequest for the PUT. function putFile(file, uploadUrl, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', uploadUrl); xhr.setRequestHeader('Content-Type', file.type); xhr.upload.onprogress = (event) => { if (event.lengthComputable) onProgress(event.loaded / event.total); }; xhr.onload = () => xhr.status < 300 ? resolve() : reject(new Error(`UPLOAD_FAILED_${xhr.status}`)); xhr.onerror = () => reject(new Error('UPLOAD_NETWORK_ERROR')); xhr.send(file); }); } // The order response holds a single `file`, not a `files` array. await putFile(file, order.file.uploadUrl, (ratio) => setProgress(ratio)); ``` Live keys receive S3 presigned URLs. Test keys receive `https://api.bizmori.com/api/v2/test-uploads/{signedToken}`; both expire after **1 hour** and accept the same unauthenticated PUT. Test uploads are streamed only, not stored or processed. Use [Refresh URLs](/api-reference/orders/refresh-urls) if a URL expires. ## Step 3: Confirm the order After uploading, call confirm to start detection processing: ```bash cURL theme={null} CONFIRM_IDEMPOTENCY_KEY=$(uuidgen | tr '[:upper:]' '[:lower:]') curl -X POST https://api.bizmori.com/api/v2/orders/ai-detection/confirm \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"idempotencyKey\": \"$CONFIRM_IDEMPOTENCY_KEY\", \"orderId\": \"123456789\"}" ``` ```javascript Node.js theme={null} const confirmIdempotencyKey = randomUUID(); await fetch('https://api.bizmori.com/api/v2/orders/ai-detection/confirm', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_TOKEN', 'Content-Type': 'application/json', }, body: JSON.stringify({ idempotencyKey: confirmIdempotencyKey, orderId: '123456789', }), }); ``` ```jsx React theme={null} await api('/orders/ai-detection/confirm', { method: 'POST', idempotencyKey: crypto.randomUUID(), body: { orderId: order.orderId }, }); ``` ```python Python theme={null} confirm_idempotency_key = str(uuid.uuid4()) requests.post( 'https://api.bizmori.com/api/v2/orders/ai-detection/confirm', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, json={'idempotencyKey': confirm_idempotency_key, 'orderId': '123456789'}, ) ``` ## Step 4: Check the result Poll the order or use [webhooks](/webhooks) to receive a push notification when detection completes. The detection result is included directly in the order detail — there is no separate download step. ```bash cURL theme={null} curl https://api.bizmori.com/api/v2/orders/123456789 \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript Node.js theme={null} const res = await fetch('https://api.bizmori.com/api/v2/orders/123456789', { headers: { 'Authorization': 'Bearer YOUR_API_TOKEN' }, }); const { data } = await res.json(); // data.probability: 0-1 // data.statusCode: 'likely_ai' | 'uncertain_ai' | 'uncertain_real' | 'likely_real' ``` ```jsx React theme={null} const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // Back off as the wait grows: quick answers stay quick, long jobs stop // hammering the API. Give up after 10 minutes rather than polling forever. async function pollOrder(orderId, { onStatus, timeoutMs = 10 * 60 * 1000 } = {}) { const deadline = Date.now() + timeoutMs; let delay = 2000; while (Date.now() < deadline) { const order = await api(`/orders/${orderId}`); onStatus?.(order.status); if (['complete', 'failed', 'expired'].includes(order.status)) return order; await sleep(delay); delay = Math.min(delay * 1.5, 15000); } throw new Error('ORDER_POLL_TIMEOUT'); } ``` ```python Python theme={null} res = requests.get( 'https://api.bizmori.com/api/v2/orders/123456789', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, ) data = res.json()['data'] probability = data['probability'] status_code = data['statusCode'] ``` **Response:** ```json theme={null} { "data": { "type": "aiDetection", "orderId": "123456789", "status": "complete", "probability": 0.92, "statusCode": "likely_ai", "heatmapUrl": "https://s3.amazonaws.com/...", "overlayUrl": "https://s3.amazonaws.com/..." } } ``` ### Reading the result The `probability` field (0–1) indicates how likely the image is AI-generated. The `statusCode` provides a human-readable interpretation: | `statusCode` | Probability range | Meaning | | ---------------- | ----------------- | --------------------------- | | `likely_real` | \< 0.25 | Highly likely human-created | | `uncertain_real` | 0.25 – 0.5 | Likely human-created | | `uncertain_ai` | 0.5 – 0.75 | Likely AI-generated | | `likely_ai` | ≥ 0.75 | Highly likely AI-generated | `heatmapUrl` and `overlayUrl` are only present if you requested them via `options.generateHeatmap` and `options.generateOverlay` in Step 1. ## Full React example Everything above, wired into one component: image upload with progress, backoff polling, and the detection result. No dependencies beyond React. ```jsx AiDetectionChecker.jsx theme={null} import { useRef, useState } from 'react'; const API_BASE = 'https://api.bizmori.com/api/v2'; const API_TOKEN = import.meta.env.VITE_MORI_API_TOKEN; const ACCEPT_FORMATS = '.jpg,.jpeg,.png,.webp,.tiff,.bmp'; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function api(path, { method = 'GET', body, idempotencyKey } = {}) { // Retries reuse the caller's idempotency key, so a retried create can never // produce a second order. Auth failures surface as `AUTH_*` codes — retrying // those will not help, so they fall through to the throw below. for (let attempt = 0; ; attempt++) { const res = await fetch(`${API_BASE}${path}`, { method, headers: { Authorization: `Bearer ${API_TOKEN}`, ...(body && { 'Content-Type': 'application/json' }), }, body: body && JSON.stringify({ idempotencyKey, ...body }), }); if (!res.ok) { const { code } = await res.json().catch(() => ({})); // 429 covers two cases: a transient rate limit, and PLAN_LIMIT_EXCEEDED, // which means the plan quota is spent and will never clear on retry. if (res.status === 429 && code !== 'PLAN_LIMIT_EXCEEDED' && attempt < 3) { await sleep(2 ** attempt * 1000); continue; } throw new Error(code ?? `HTTP_${res.status}`); } return (await res.json()).data; } } function putFile(file, uploadUrl, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', uploadUrl); xhr.setRequestHeader('Content-Type', file.type); xhr.upload.onprogress = (event) => { if (event.lengthComputable) onProgress(event.loaded / event.total); }; xhr.onload = () => xhr.status < 300 ? resolve() : reject(new Error(`UPLOAD_FAILED_${xhr.status}`)); xhr.onerror = () => reject(new Error('UPLOAD_NETWORK_ERROR')); xhr.send(file); }); } async function pollOrder(orderId, { onStatus, timeoutMs = 10 * 60 * 1000 } = {}) { const deadline = Date.now() + timeoutMs; let delay = 2000; while (Date.now() < deadline) { const order = await api(`/orders/${orderId}`); onStatus?.(order.status); if (['complete', 'failed', 'expired'].includes(order.status)) return order; await sleep(delay); delay = Math.min(delay * 1.5, 15000); } throw new Error('ORDER_POLL_TIMEOUT'); } export default function AiDetectionChecker() { const [file, setFile] = useState(null); const [progress, setProgress] = useState(0); const [status, setStatus] = useState('idle'); const [result, setResult] = useState(null); const [error, setError] = useState(null); const [orderId, setOrderId] = useState(null); const inFlight = useRef(false); const busy = status !== 'idle' && status !== 'complete'; async function detectImage(event) { event.preventDefault(); if (inFlight.current || !file) return; inFlight.current = true; // Snapshot the selection. The user can swap the file while requests are // in flight, and `selected` has to stay paired with the order it created. const selected = file; setError(null); setOrderId(null); setResult(null); setProgress(0); try { setStatus('creating order'); const order = await api('/orders/ai-detection', { method: 'POST', idempotencyKey: crypto.randomUUID(), body: { fileName: selected.name, options: { generateHeatmap: true, generateOverlay: true }, }, }); setOrderId(order.orderId); setStatus('uploading'); await putFile(selected, order.file.uploadUrl, setProgress); setStatus('confirming'); await api('/orders/ai-detection/confirm', { method: 'POST', idempotencyKey: crypto.randomUUID(), body: { orderId: order.orderId }, }); const finished = await pollOrder(order.orderId, { onStatus: setStatus }); // `expired` is terminal too: the cleanup job drops the files after 7 days. if (finished.status !== 'complete') throw new Error(`ORDER_${finished.status.toUpperCase()}`); setResult(finished); setStatus('complete'); } catch (caught) { setError(caught.message); setStatus('idle'); } finally { inFlight.current = false; } } return (
{ setFile(event.target.files[0] ?? null); setError(null); setProgress(0); setResult(null); }} /> {status !== 'idle' &&

Status: {status}

} {status === 'uploading' &&

Upload progress: {Math.round(progress * 100)}%

} {result && (

Probability: {Math.round(result.probability * 100)}% — {result.statusCode}

{result.heatmapUrl && AI detection heatmap} {result.overlayUrl && AI detection overlay}
)} {error &&

Failed: {error}{orderId && ` (orderId: ${orderId})`}

}
); } ``` `import.meta.env.VITE_MORI_API_TOKEN` is Vite's syntax. Use `process.env.NEXT_PUBLIC_MORI_API_TOKEN` on Next.js, or whatever your bundler exposes to client code. `crypto.randomUUID()` needs a secure context: HTTPS and `localhost` are fine, a plain-HTTP LAN address is not. ## Error handling | HTTP Status | Meaning | Action | | ----------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `400` | Invalid request | Check parameters and file format | | `401` | Authentication failed | Verify your API key | | `429` | Rate limit, or `PLAN_LIMIT_EXCEEDED` when the plan quota is spent | Check the `code` field. A rate limit clears on retry; `PLAN_LIMIT_EXCEEDED` does not — upgrade the plan | For the complete error code reference, see [Error Codes](/errors). ## Next steps Protect images from AI training and generation. Embed invisible watermarks into images. Detect and extract watermarks from images. Set up webhooks to get notified when processing completes. # Anti-AI Source: https://docs.bizmori.com/quickstart/anti-ai Protect your images from AI with the Anti-AI API This guide walks you through creating an **Anti-AI protection order** — from uploading an image to downloading the protected result. Every endpoint here is plain HTTPS and JSON, so the same calls work from a server or straight from the browser. Each step includes a **React** tab, and a [full copy-paste component](#full-react-example) is at the bottom of the page. You can also provide image URLs directly instead of uploading files. See the [Create Anti-AI order API](/api-reference/anti-ai/create-order) for URL mode details. ## Prerequisites * A BIZ MORI API key ([get one here](https://app.bizmori.com/keys)) * An image file to protect (`jpeg`, `jpg`, `png`, `webp`, `tiff`, or `bmp`) Use the automatically issued [`sk_test_` key](/test-api-keys) to exercise the order lifecycle without running Anti-AI processing or consuming credits. Test uploads discard file contents and do not produce a downloadable result. Use a live API key when you need an actual protected image. ## Step 1: Create an order Create an order and receive presigned S3 URLs for uploading your files. ```bash cURL theme={null} ORDER_IDEMPOTENCY_KEY=$(uuidgen | tr '[:upper:]' '[:lower:]') curl -X POST https://api.bizmori.com/api/v2/orders/anti-ai \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "idempotencyKey": "'$ORDER_IDEMPOTENCY_KEY'", "files": [{ "fileName": "photo.jpg" }], "options": { "strength": "high" } }' ``` ```javascript Node.js theme={null} import { randomUUID } from 'node:crypto'; const orderIdempotencyKey = randomUUID(); const response = await fetch('https://api.bizmori.com/api/v2/orders/anti-ai', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_TOKEN', 'Content-Type': 'application/json', }, body: JSON.stringify({ idempotencyKey: orderIdempotencyKey, files: [{ fileName: 'photo.jpg' }], options: { strength: 'high' }, }), }); const { data } = await response.json(); ``` ```jsx React theme={null} // One helper for every JSON call. The API returns `{ code: "ERROR_CODE" }` // on failure, so surface that code instead of a bare status number. const API_BASE = 'https://api.bizmori.com/api/v2'; const API_TOKEN = import.meta.env.VITE_MORI_API_TOKEN; async function api(path, { method = 'GET', body, idempotencyKey } = {}) { const res = await fetch(`${API_BASE}${path}`, { method, headers: { Authorization: `Bearer ${API_TOKEN}`, ...(body && { 'Content-Type': 'application/json' }), }, body: body && JSON.stringify({ idempotencyKey, ...body }), }); if (!res.ok) { const { code } = await res.json().catch(() => ({})); throw new Error(code ?? `HTTP_${res.status}`); } return (await res.json()).data; } // `files` is the File[] the user picked const order = await api('/orders/anti-ai', { method: 'POST', idempotencyKey: crypto.randomUUID(), body: { files: files.map((file) => ({ fileName: file.name })), options: { strength: 'high' }, }, }); ``` ```python Python theme={null} import uuid import requests order_idempotency_key = str(uuid.uuid4()) res = requests.post( 'https://api.bizmori.com/api/v2/orders/anti-ai', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, json={ 'idempotencyKey': order_idempotency_key, 'files': [{'fileName': 'photo.jpg'}], 'options': {'strength': 'high'}, }, ) data = res.json()['data'] ``` **Response:** ```json theme={null} { "data": { "orderName": "anti_ai_2026-02-19", "orderId": "123456789", "status": "pending", "files": [ { "fileId": 1, "fileName": "photo.jpg", "uploadUrl": "https://s3.amazonaws.com/...", "fileKey": "temp/123456789/0/photo.jpg" } ] } } ``` ## Step 2: Upload files PUT your file to the presigned `uploadUrl` from Step 1. This is a direct S3 upload — **no Authorization header needed**. ```bash cURL theme={null} curl -X PUT "https://s3.amazonaws.com/..." \ -H "Content-Type: image/jpeg" \ --data-binary @photo.jpg ``` ```jsx React theme={null} // fetch() cannot report upload progress, so use XMLHttpRequest for the PUT. function putFile(file, uploadUrl, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', uploadUrl); xhr.setRequestHeader('Content-Type', file.type); xhr.upload.onprogress = (event) => { if (event.lengthComputable) onProgress(event.loaded / event.total); }; xhr.onload = () => xhr.status < 300 ? resolve() : reject(new Error(`UPLOAD_FAILED_${xhr.status}`)); xhr.onerror = () => reject(new Error('UPLOAD_NETWORK_ERROR')); xhr.send(file); }); } // The response lists files in the order you sent them, so `files[index]` // belongs to `order.files[index].uploadUrl`. await Promise.all( files.map((file, index) => putFile(file, order.files[index].uploadUrl, (ratio) => setProgress((prev) => ({ ...prev, [index]: ratio })), ), ), ); ``` Live keys receive S3 presigned URLs. Test keys receive `https://api.bizmori.com/api/v2/test-uploads/{signedToken}`; both expire after **1 hour** and accept the same unauthenticated PUT. Test uploads are streamed only, not stored or processed. Use [Refresh URLs](/api-reference/orders/refresh-urls) if a URL expires. ## Step 3: Confirm the order After uploading all files, call confirm to start processing: ```bash cURL theme={null} CONFIRM_IDEMPOTENCY_KEY=$(uuidgen | tr '[:upper:]' '[:lower:]') curl -X POST https://api.bizmori.com/api/v2/orders/anti-ai/confirm \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"idempotencyKey\": \"$CONFIRM_IDEMPOTENCY_KEY\", \"orderId\": \"123456789\"}" ``` ```javascript Node.js theme={null} const confirmIdempotencyKey = randomUUID(); await fetch('https://api.bizmori.com/api/v2/orders/anti-ai/confirm', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_TOKEN', 'Content-Type': 'application/json', }, body: JSON.stringify({ idempotencyKey: confirmIdempotencyKey, orderId: '123456789', }), }); ``` ```jsx React theme={null} await api('/orders/anti-ai/confirm', { method: 'POST', idempotencyKey: crypto.randomUUID(), body: { orderId: order.orderId }, }); ``` ```python Python theme={null} confirm_idempotency_key = str(uuid.uuid4()) requests.post( 'https://api.bizmori.com/api/v2/orders/anti-ai/confirm', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, json={'idempotencyKey': confirm_idempotency_key, 'orderId': '123456789'}, ) ``` ## Step 4: Check order status Poll the order or use [webhooks](/webhooks) to receive a push notification when processing completes. ```bash cURL theme={null} curl https://api.bizmori.com/api/v2/orders/123456789 \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript Node.js theme={null} const res = await fetch('https://api.bizmori.com/api/v2/orders/123456789', { headers: { 'Authorization': 'Bearer YOUR_API_TOKEN' }, }); const { data } = await res.json(); // data.status: 'pending' | 'inProgress' | 'complete' | 'failed' ``` ```jsx React theme={null} const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // Back off as the wait grows: quick answers stay quick, long jobs stop // hammering the API. Give up after 10 minutes rather than polling forever. async function pollOrder(orderId, { onStatus, timeoutMs = 10 * 60 * 1000 } = {}) { const deadline = Date.now() + timeoutMs; let delay = 2000; while (Date.now() < deadline) { const order = await api(`/orders/${orderId}`); onStatus?.(order.status); if (['complete', 'failed', 'expired'].includes(order.status)) return order; await sleep(delay); delay = Math.min(delay * 1.5, 15000); } throw new Error('ORDER_POLL_TIMEOUT'); } ``` ```python Python theme={null} res = requests.get( 'https://api.bizmori.com/api/v2/orders/123456789', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, ) status = res.json()['data']['status'] ``` Order statuses: | Status | Meaning | | ------------ | ----------------------- | | `pending` | Waiting for file upload | | `inProgress` | Processing | | `complete` | Ready for download | | `failed` | Processing failed | ## Step 5: Download the result Once `status` is `complete`, fetch the download URL: ```bash cURL theme={null} curl https://api.bizmori.com/api/v2/orders/123456789/download \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript Node.js theme={null} const res = await fetch('https://api.bizmori.com/api/v2/orders/123456789/download', { headers: { 'Authorization': 'Bearer YOUR_API_TOKEN' }, }); const { data } = await res.json(); // data.url — valid for 7 days ``` ```jsx React theme={null} const { url } = await api(`/orders/${order.orderId}/download`); // Hand it to an — the URL is valid for 7 days setDownloadUrl(url); ``` ```python Python theme={null} res = requests.get( 'https://api.bizmori.com/api/v2/orders/123456789/download', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, ) download_url = res.json()['data']['url'] ``` **Response:** ```json theme={null} { "data": { "url": "https://s3.amazonaws.com/..." } } ``` The `url` is a presigned S3 URL valid for **7 days**. Download the protected file directly from this URL. ## Full React example Everything above, wired into one component: multiple files, per-file upload progress, backoff polling, and a download link. No dependencies beyond React. ```jsx AntiAiUploader.jsx theme={null} import { useRef, useState } from 'react'; const API_BASE = 'https://api.bizmori.com/api/v2'; const API_TOKEN = import.meta.env.VITE_MORI_API_TOKEN; const ACCEPT_FORMATS = '.jpg,.jpeg,.png,.webp,.tiff,.bmp'; const MAX_FILES = 100; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function api(path, { method = 'GET', body, idempotencyKey } = {}) { // Retries reuse the caller's idempotency key, so a retried create can never // produce a second order. Auth failures surface as `AUTH_*` codes — retrying // those will not help, so they fall through to the throw below. for (let attempt = 0; ; attempt++) { const res = await fetch(`${API_BASE}${path}`, { method, headers: { Authorization: `Bearer ${API_TOKEN}`, ...(body && { 'Content-Type': 'application/json' }), }, body: body && JSON.stringify({ idempotencyKey, ...body }), }); if (!res.ok) { const { code } = await res.json().catch(() => ({})); // 429 covers two cases: a transient rate limit, and PLAN_LIMIT_EXCEEDED, // which means the plan quota is spent and will never clear on retry. if (res.status === 429 && code !== 'PLAN_LIMIT_EXCEEDED' && attempt < 3) { await sleep(2 ** attempt * 1000); continue; } throw new Error(code ?? `HTTP_${res.status}`); } return (await res.json()).data; } } function putFile(file, uploadUrl, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', uploadUrl); xhr.setRequestHeader('Content-Type', file.type); xhr.upload.onprogress = (event) => { if (event.lengthComputable) onProgress(event.loaded / event.total); }; xhr.onload = () => xhr.status < 300 ? resolve() : reject(new Error(`UPLOAD_FAILED_${xhr.status}`)); xhr.onerror = () => reject(new Error('UPLOAD_NETWORK_ERROR')); xhr.send(file); }); } async function pollOrder(orderId, { onStatus, timeoutMs = 10 * 60 * 1000 } = {}) { const deadline = Date.now() + timeoutMs; let delay = 2000; while (Date.now() < deadline) { const order = await api(`/orders/${orderId}`); onStatus?.(order.status); if (['complete', 'failed', 'expired'].includes(order.status)) return order; await sleep(delay); delay = Math.min(delay * 1.5, 15000); } throw new Error('ORDER_POLL_TIMEOUT'); } export default function AntiAiUploader() { const [files, setFiles] = useState([]); const [progress, setProgress] = useState({}); const [status, setStatus] = useState('idle'); const [downloadUrl, setDownloadUrl] = useState(null); const [error, setError] = useState(null); const [orderId, setOrderId] = useState(null); const inFlight = useRef(false); const busy = status !== 'idle' && status !== 'complete'; async function protectImages(event) { event.preventDefault(); if (inFlight.current || files.length === 0) return; inFlight.current = true; // Snapshot the selection. The user can swap files while requests are in // flight, and selected[i] has to stay paired with uploadUrls[i]. const selected = files; setError(null); setOrderId(null); setDownloadUrl(null); setProgress({}); try { setStatus('creating order'); const order = await api('/orders/anti-ai', { method: 'POST', idempotencyKey: crypto.randomUUID(), body: { files: selected.map((file) => ({ fileName: file.name })), options: { strength: 'high' }, }, }); setOrderId(order.orderId); setStatus('uploading'); await Promise.all( selected.map((file, index) => putFile(file, order.files[index].uploadUrl, (ratio) => setProgress((prev) => ({ ...prev, [index]: ratio })), ), ), ); setStatus('confirming'); await api('/orders/anti-ai/confirm', { method: 'POST', idempotencyKey: crypto.randomUUID(), body: { orderId: order.orderId }, }); const finished = await pollOrder(order.orderId, { onStatus: setStatus }); // `expired` is terminal too: the cleanup job drops the files after 7 days. if (finished.status !== 'complete') throw new Error(`ORDER_${finished.status.toUpperCase()}`); setStatus('fetching download URL'); const { url } = await api(`/orders/${order.orderId}/download`); setDownloadUrl(url); setStatus('complete'); } catch (caught) { setError(caught.message); setStatus('idle'); } finally { inFlight.current = false; } } return (
{ setFiles([...event.target.files].slice(0, MAX_FILES)); setDownloadUrl(null); setError(null); setProgress({}); }} /> {status !== 'idle' &&

Status: {status}

}
    {files.map((file, index) => (
  • {file.name} — {Math.round((progress[index] ?? 0) * 100)}%
  • ))}
{downloadUrl && (
Download protected images )} {error &&

Failed: {error}{orderId && ` (orderId: ${orderId})`}

}
); } ``` `import.meta.env.VITE_MORI_API_TOKEN` is Vite's syntax. Use `process.env.NEXT_PUBLIC_MORI_API_TOKEN` on Next.js, or whatever your bundler exposes to client code. `crypto.randomUUID()` needs a secure context: HTTPS and `localhost` are fine, a plain-HTTP LAN address is not. Test orders do not produce a result file: their `downloadUrl` is `null`, and the download endpoint can return `PROCESSED_FILE_NOT_FOUND`. ## Error handling | HTTP Status | Meaning | Action | | ----------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `400` | Invalid request | Check parameters and file format | | `401` | Authentication failed | Verify your API key | | `429` | Rate limit, or `PLAN_LIMIT_EXCEEDED` when the plan quota is spent | Check the `code` field. A rate limit clears on retry; `PLAN_LIMIT_EXCEEDED` does not — upgrade the plan | For the complete error code reference, see [Error Codes](/errors). ## Next steps Embed invisible watermarks into your images. Detect and extract watermarks from images. Detect AI-generated images with probability scores. Set up webhooks to get notified when processing completes. # Watermark Embed Source: https://docs.bizmori.com/quickstart/watermark-embed Embed invisible watermarks into your images This guide walks you through embedding an invisible watermark into an image — from creating an order to downloading the watermarked result. Every endpoint here is plain HTTPS and JSON, so the same calls work from a server or straight from the browser. Each step includes a **React** tab, and a [full copy-paste component](#full-react-example) is at the bottom of the page. ## Prerequisites * A BIZ MORI API key ([get one here](https://app.bizmori.com/keys)) * An image or PDF file to watermark (`jpeg`, `jpg`, `png`, `webp`, `tiff`, `bmp`, or `pdf`) Use the automatically issued [`sk_test_` key](/test-api-keys) to exercise the order lifecycle without running watermark processing or consuming credits. Test uploads discard file contents and do not produce a downloadable result. Use a live API key when you need an actual watermarked file. ## Step 1: Create an order Create a watermark embed order with your watermark text. ```bash cURL theme={null} ORDER_IDEMPOTENCY_KEY=$(uuidgen | tr '[:upper:]' '[:lower:]') curl -X POST https://api.bizmori.com/api/v2/orders/wtr-embed \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "idempotencyKey": "'$ORDER_IDEMPOTENCY_KEY'", "files": [ { "fileName": "photo.jpg", "watermarks": [{ "text": "MORI_WATERMARK" }] } ] }' ``` ```javascript Node.js theme={null} import { randomUUID } from 'node:crypto'; const orderIdempotencyKey = randomUUID(); const response = await fetch('https://api.bizmori.com/api/v2/orders/wtr-embed', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_TOKEN', 'Content-Type': 'application/json', }, body: JSON.stringify({ idempotencyKey: orderIdempotencyKey, files: [ { fileName: 'photo.jpg', watermarks: [{ text: 'MORI_WATERMARK' }], }, ], }), }); const { data } = await response.json(); ``` ```jsx React theme={null} // One helper for every JSON call. The API returns `{ code: "ERROR_CODE" }` // on failure, so surface that code instead of a bare status number. const API_BASE = 'https://api.bizmori.com/api/v2'; const API_TOKEN = import.meta.env.VITE_MORI_API_TOKEN; async function api(path, { method = 'GET', body, idempotencyKey } = {}) { const res = await fetch(`${API_BASE}${path}`, { method, headers: { Authorization: `Bearer ${API_TOKEN}`, ...(body && { 'Content-Type': 'application/json' }), }, body: body && JSON.stringify({ idempotencyKey, ...body }), }); if (!res.ok) { const { code } = await res.json().catch(() => ({})); throw new Error(code ?? `HTTP_${res.status}`); } return (await res.json()).data; } // `file` is the single File the user picked, `watermarkTexts` is a string[] const order = await api('/orders/wtr-embed', { method: 'POST', idempotencyKey: crypto.randomUUID(), body: { files: [ { fileName: file.name, watermarks: watermarkTexts.map((text) => ({ text })), }, ], }, }); ``` ```python Python theme={null} import uuid import requests order_idempotency_key = str(uuid.uuid4()) res = requests.post( 'https://api.bizmori.com/api/v2/orders/wtr-embed', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, json={ 'idempotencyKey': order_idempotency_key, 'files': [ { 'fileName': 'photo.jpg', 'watermarks': [{'text': 'MORI_WATERMARK'}], } ], }, ) data = res.json()['data'] ``` **Response:** ```json theme={null} { "data": { "orderName": "wtr_embed_2026-03-18", "orderId": "123456789", "status": "pending", "files": [ { "fileId": 1, "fileName": "photo.jpg", "uploadUrl": "https://s3.amazonaws.com/...", "fileKey": "wtr-embed/123456789/images/1/photo.jpg", "fileFormat": "JPG", "fileType": "IMG" } ] } } ``` ## Step 2: Upload files PUT your file to the presigned `uploadUrl` from Step 1. This is a direct S3 upload — **no Authorization header needed**. ```bash cURL theme={null} curl -X PUT "https://s3.amazonaws.com/..." \ -H "Content-Type: image/jpeg" \ --data-binary @photo.jpg ``` ```jsx React theme={null} // fetch() cannot report upload progress, so use XMLHttpRequest for the PUT. function putFile(file, uploadUrl, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', uploadUrl); xhr.setRequestHeader('Content-Type', file.type); xhr.upload.onprogress = (event) => { if (event.lengthComputable) onProgress(event.loaded / event.total); }; xhr.onload = () => xhr.status < 300 ? resolve() : reject(new Error(`UPLOAD_FAILED_${xhr.status}`)); xhr.onerror = () => reject(new Error('UPLOAD_NETWORK_ERROR')); xhr.send(file); }); } // There's exactly one file, so upload progress is a single number. await putFile(file, order.files[0].uploadUrl, (ratio) => setProgress(ratio)); ``` **No confirm step needed.** Unlike Anti-AI and AI Detection, Watermark Embed processing starts automatically after your file upload completes. Proceed directly to checking the order status. Live keys receive S3 presigned URLs. Test keys receive `https://api.bizmori.com/api/v2/test-uploads/{signedToken}`; both expire after **1 hour** and accept the same unauthenticated PUT. Test uploads are streamed only, not stored or processed. Use [Refresh URLs](/api-reference/orders/refresh-urls) if a URL expires. ## Step 3: Check order status & download result Poll the order or use [webhooks](/webhooks) to receive a push notification when processing completes. ```bash cURL theme={null} curl https://api.bizmori.com/api/v2/orders/123456789 \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript Node.js theme={null} const res = await fetch('https://api.bizmori.com/api/v2/orders/123456789', { headers: { 'Authorization': 'Bearer YOUR_API_TOKEN' }, }); const { data } = await res.json(); // data.status: 'pending' | 'inProgress' | 'complete' | 'failed' ``` ```jsx React theme={null} const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // Back off as the wait grows: quick answers stay quick, long jobs stop // hammering the API. Give up after 10 minutes rather than polling forever. async function pollOrder(orderId, { onStatus, timeoutMs = 10 * 60 * 1000 } = {}) { const deadline = Date.now() + timeoutMs; let delay = 2000; while (Date.now() < deadline) { const order = await api(`/orders/${orderId}`); onStatus?.(order.status); if (['complete', 'failed', 'expired'].includes(order.status)) return order; await sleep(delay); delay = Math.min(delay * 1.5, 15000); } throw new Error('ORDER_POLL_TIMEOUT'); } ``` ```python Python theme={null} res = requests.get( 'https://api.bizmori.com/api/v2/orders/123456789', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, ) status = res.json()['data']['status'] ``` | Status | Meaning | | ------------ | ----------------------- | | `pending` | Waiting for file upload | | `inProgress` | Processing | | `complete` | Ready for download | | `failed` | Processing failed | Once `status` is `complete`, fetch the download URL: ```bash cURL theme={null} curl https://api.bizmori.com/api/v2/orders/123456789/download \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript Node.js theme={null} const res = await fetch('https://api.bizmori.com/api/v2/orders/123456789/download', { headers: { 'Authorization': 'Bearer YOUR_API_TOKEN' }, }); const { data } = await res.json(); // data.url — valid for 7 days ``` ```jsx React theme={null} const { url } = await api(`/orders/${order.orderId}/download`); // Hand it to an — the URL is valid for 7 days setDownloadUrl(url); ``` ```python Python theme={null} res = requests.get( 'https://api.bizmori.com/api/v2/orders/123456789/download', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, ) download_url = res.json()['data']['url'] ``` **Response:** ```json theme={null} { "data": { "url": "https://s3.amazonaws.com/..." } } ``` The `url` is a presigned S3 URL valid for **7 days**. ## Full React example Everything above, wired into one component: single-file upload with progress, a watermark text list with add/remove and duplicate detection, backoff polling, and a download link. No dependencies beyond React. ```jsx WatermarkEmbedUploader.jsx theme={null} import { useMemo, useRef, useState } from 'react'; const API_BASE = 'https://api.bizmori.com/api/v2'; const API_TOKEN = import.meta.env.VITE_MORI_API_TOKEN; const ACCEPT_FORMATS = '.jpg,.jpeg,.png,.webp,.tiff,.bmp,.pdf'; const MAX_WATERMARKS = 10; const MAX_TEXT_LENGTH = 1000; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function api(path, { method = 'GET', body, idempotencyKey } = {}) { // Retries reuse the caller's idempotency key, so a retried create can never // produce a second order. Auth failures surface as `AUTH_*` codes — retrying // those will not help, so they fall through to the throw below. for (let attempt = 0; ; attempt++) { const res = await fetch(`${API_BASE}${path}`, { method, headers: { Authorization: `Bearer ${API_TOKEN}`, ...(body && { 'Content-Type': 'application/json' }), }, body: body && JSON.stringify({ idempotencyKey, ...body }), }); if (!res.ok) { const { code } = await res.json().catch(() => ({})); // 429 covers two cases: a transient rate limit, and PLAN_LIMIT_EXCEEDED, // which means the plan quota is spent and will never clear on retry. if (res.status === 429 && code !== 'PLAN_LIMIT_EXCEEDED' && attempt < 3) { await sleep(2 ** attempt * 1000); continue; } throw new Error(code ?? `HTTP_${res.status}`); } return (await res.json()).data; } } function putFile(file, uploadUrl, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', uploadUrl); xhr.setRequestHeader('Content-Type', file.type); xhr.upload.onprogress = (event) => { if (event.lengthComputable) onProgress(event.loaded / event.total); }; xhr.onload = () => xhr.status < 300 ? resolve() : reject(new Error(`UPLOAD_FAILED_${xhr.status}`)); xhr.onerror = () => reject(new Error('UPLOAD_NETWORK_ERROR')); xhr.send(file); }); } async function pollOrder(orderId, { onStatus, timeoutMs = 10 * 60 * 1000 } = {}) { const deadline = Date.now() + timeoutMs; let delay = 2000; while (Date.now() < deadline) { const order = await api(`/orders/${orderId}`); onStatus?.(order.status); if (['complete', 'failed', 'expired'].includes(order.status)) return order; await sleep(delay); delay = Math.min(delay * 1.5, 15000); } throw new Error('ORDER_POLL_TIMEOUT'); } export default function WatermarkEmbedUploader() { const [file, setFile] = useState(null); // Lazy initializer: without it, crypto.randomUUID() would run on every render. const [watermarks, setWatermarks] = useState(() => [{ id: crypto.randomUUID(), text: '' }]); const [progress, setProgress] = useState(0); const [status, setStatus] = useState('idle'); const [downloadUrl, setDownloadUrl] = useState(null); const [error, setError] = useState(null); const [orderId, setOrderId] = useState(null); const inFlight = useRef(false); const busy = status !== 'idle' && status !== 'complete'; // Flags every text after the first occurrence as a duplicate. Blank rows // are exempt — they're already caught by the "no blank text" rule below. const duplicateIds = useMemo(() => { const seen = new Map(); const duplicates = new Set(); watermarks.forEach((watermark) => { const text = watermark.text.trim(); if (text === '') return; if (seen.has(text)) duplicates.add(watermark.id); else seen.set(text, watermark.id); }); return duplicates; }, [watermarks]); const canSubmit = file !== null && watermarks.length > 0 && watermarks.every((watermark) => watermark.text.trim() !== '') && duplicateIds.size === 0 && !busy; function updateWatermark(id, text) { setWatermarks((prev) => prev.map((watermark) => (watermark.id === id ? { ...watermark, text } : watermark))); } function addWatermark() { if (watermarks.length >= MAX_WATERMARKS) return; setWatermarks((prev) => [...prev, { id: crypto.randomUUID(), text: '' }]); } function removeWatermark(id) { setWatermarks((prev) => prev.filter((watermark) => watermark.id !== id)); } async function embedWatermarks(event) { event.preventDefault(); if (inFlight.current || !canSubmit) return; inFlight.current = true; // Snapshot the selection. The user can edit the form while requests are // in flight, and the upload must use the values as they were on submit. const selectedFile = file; const selectedWatermarks = watermarks; setError(null); setOrderId(null); setDownloadUrl(null); setProgress(0); try { setStatus('creating order'); const order = await api('/orders/wtr-embed', { method: 'POST', idempotencyKey: crypto.randomUUID(), body: { files: [ { fileName: selectedFile.name, watermarks: selectedWatermarks.map((watermark) => ({ text: watermark.text.trim() })), }, ], }, }); setOrderId(order.orderId); setStatus('uploading'); await putFile(selectedFile, order.files[0].uploadUrl, setProgress); // No confirm step — processing starts automatically once the upload completes. const finished = await pollOrder(order.orderId, { onStatus: setStatus }); // `expired` is terminal too: the cleanup job drops the files after 7 days. if (finished.status !== 'complete') throw new Error(`ORDER_${finished.status.toUpperCase()}`); setStatus('fetching download URL'); const { url } = await api(`/orders/${order.orderId}/download`); setDownloadUrl(url); setStatus('complete'); } catch (caught) { setError(caught.message); setStatus('idle'); } finally { inFlight.current = false; } } return (
{ setFile(event.target.files[0] ?? null); setError(null); setProgress(0); setDownloadUrl(null); }} />
    {watermarks.map((watermark, index) => (
  • updateWatermark(watermark.id, event.target.value)} style={{ borderColor: duplicateIds.has(watermark.id) ? 'red' : undefined }} />
  • ))}
{status !== 'idle' &&

Status: {status}

} {status === 'uploading' &&

Upload progress: {Math.round(progress * 100)}%

} {downloadUrl && (
Download watermarked image )} {error &&

Failed: {error}{orderId && ` (orderId: ${orderId})`}

}
); } ``` `import.meta.env.VITE_MORI_API_TOKEN` is Vite's syntax. Use `process.env.NEXT_PUBLIC_MORI_API_TOKEN` on Next.js, or whatever your bundler exposes to client code. `crypto.randomUUID()` needs a secure context: HTTPS and `localhost` are fine, a plain-HTTP LAN address is not. Test orders do not produce a result file: their `downloadUrl` is `null`, and the download endpoint can return `PROCESSED_FILE_NOT_FOUND`. ## Error handling | HTTP Status | Meaning | Action | | ----------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `400` | Invalid request | Check parameters and file format | | `401` | Authentication failed | Verify your API key | | `429` | Rate limit, or `PLAN_LIMIT_EXCEEDED` when the plan quota is spent | Check the `code` field. A rate limit clears on retry; `PLAN_LIMIT_EXCEEDED` does not — upgrade the plan | For the complete error code reference, see [Error Codes](/errors). ## Next steps Protect images from AI training and generation. Detect and extract watermarks from images. Detect AI-generated images with probability scores. Set up webhooks to get notified when processing completes. # Watermark Extract Source: https://docs.bizmori.com/quickstart/watermark-extract Detect and extract watermarks from images This guide walks you through detecting a watermark in an image — from creating an order to reading the extraction result. Every endpoint here is plain HTTPS and JSON, so the same calls work from a server or straight from the browser. Each step includes a **React** tab, and a [full copy-paste component](#full-react-example) is at the bottom of the page. ## Prerequisites * A BIZ MORI API key ([get one here](https://app.bizmori.com/keys)) * A watermarked image file (`jpeg`, `jpg`, `png`, `webp`, `bmp`, or `tiff`) or PDF Use the automatically issued [`sk_test_` key](/test-api-keys) to exercise the order lifecycle without running watermark extraction or consuming credits. Test uploads discard file contents and return deterministic detection states. Use a live API key when you need to extract a watermark from an actual file. ## Step 1: Create an order Create a watermark extraction order with your file. ```bash cURL theme={null} ORDER_IDEMPOTENCY_KEY=$(uuidgen | tr '[:upper:]' '[:lower:]') curl -X POST https://api.bizmori.com/api/v2/orders/wtr-extract \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "idempotencyKey": "'$ORDER_IDEMPOTENCY_KEY'", "file": { "fileName": "watermarked.jpg" } }' ``` ```javascript Node.js theme={null} import { randomUUID } from 'node:crypto'; const orderIdempotencyKey = randomUUID(); const response = await fetch('https://api.bizmori.com/api/v2/orders/wtr-extract', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_TOKEN', 'Content-Type': 'application/json', }, body: JSON.stringify({ idempotencyKey: orderIdempotencyKey, file: { fileName: 'watermarked.jpg', }, }), }); const { data } = await response.json(); ``` ```jsx React theme={null} // One helper for every JSON call. The API returns `{ code: "ERROR_CODE" }` // on failure, so surface that code instead of a bare status number. const API_BASE = 'https://api.bizmori.com/api/v2'; const API_TOKEN = import.meta.env.VITE_MORI_API_TOKEN; async function api(path, { method = 'GET', body, idempotencyKey } = {}) { const res = await fetch(`${API_BASE}${path}`, { method, headers: { Authorization: `Bearer ${API_TOKEN}`, ...(body && { 'Content-Type': 'application/json' }), }, body: body && JSON.stringify({ idempotencyKey, ...body }), }); if (!res.ok) { const { code } = await res.json().catch(() => ({})); throw new Error(code ?? `HTTP_${res.status}`); } return (await res.json()).data; } // `file` is the single File the user picked const order = await api('/orders/wtr-extract', { method: 'POST', idempotencyKey: crypto.randomUUID(), body: { file: { fileName: file.name }, }, }); ``` ```python Python theme={null} import uuid import requests order_idempotency_key = str(uuid.uuid4()) res = requests.post( 'https://api.bizmori.com/api/v2/orders/wtr-extract', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, json={ 'idempotencyKey': order_idempotency_key, 'file': { 'fileName': 'watermarked.jpg', }, }, ) data = res.json()['data'] ``` **Response:** ```json theme={null} { "data": { "orderName": "wtr_extract_2026-03-18", "orderId": "123456789", "file": { "fileId": 1, "fileName": "watermarked.jpg", "uploadUrl": "https://s3.amazonaws.com/...", "fileKey": "123/456/watermarked.jpg" } } } ``` You can include the original (pre-watermarked) image for more accurate extraction. Set `"includeOriginal": true` and provide `"originalFile": { "fileName": "original.jpg" }` in the request. This option is available for images only, not PDFs. ## Step 2: Upload the file PUT your file to the presigned `uploadUrl` from Step 1. This is a direct S3 upload — **no Authorization header needed**. ```bash cURL theme={null} curl -X PUT "https://s3.amazonaws.com/..." \ -H "Content-Type: image/jpeg" \ --data-binary @watermarked.jpg ``` ```jsx React theme={null} // fetch() cannot report upload progress, so use XMLHttpRequest for the PUT. function putFile(file, uploadUrl, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', uploadUrl); xhr.setRequestHeader('Content-Type', file.type); xhr.upload.onprogress = (event) => { if (event.lengthComputable) onProgress(event.loaded / event.total); }; xhr.onload = () => xhr.status < 300 ? resolve() : reject(new Error(`UPLOAD_FAILED_${xhr.status}`)); xhr.onerror = () => reject(new Error('UPLOAD_NETWORK_ERROR')); xhr.send(file); }); } // The order response holds a single `file`, not a `files` array. await putFile(file, order.file.uploadUrl, (ratio) => setProgress(ratio)); ``` **No confirm step needed.** Like Watermark Embed, Watermark Extract processing starts automatically after your file upload completes. Proceed directly to checking the result. Live keys receive S3 presigned URLs. Test keys receive `https://api.bizmori.com/api/v2/test-uploads/{signedToken}`; both expire after **1 hour** and accept the same unauthenticated PUT. Test uploads are streamed only, not stored or processed. Use [Refresh URLs](/api-reference/orders/refresh-urls) if a URL expires. ## Step 3: Check the result Poll the order or use [webhooks](/webhooks) to receive a push notification when extraction completes. ```bash cURL theme={null} curl https://api.bizmori.com/api/v2/orders/123456789 \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript Node.js theme={null} const res = await fetch('https://api.bizmori.com/api/v2/orders/123456789', { headers: { 'Authorization': 'Bearer YOUR_API_TOKEN' }, }); const { data } = await res.json(); ``` ```jsx React theme={null} const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // Back off as the wait grows: quick answers stay quick, long jobs stop // hammering the API. Give up after 10 minutes rather than polling forever. // Poll on `status` — the detection outcome arrives separately in `statusCode`. async function pollOrder(orderId, { onStatus, timeoutMs = 10 * 60 * 1000 } = {}) { const deadline = Date.now() + timeoutMs; let delay = 2000; while (Date.now() < deadline) { const order = await api(`/orders/${orderId}`); onStatus?.(order.status); if (['complete', 'failed', 'expired'].includes(order.status)) return order; await sleep(delay); delay = Math.min(delay * 1.5, 15000); } throw new Error('ORDER_POLL_TIMEOUT'); } ``` ```python Python theme={null} res = requests.get( 'https://api.bizmori.com/api/v2/orders/123456789', headers={'Authorization': 'Bearer YOUR_API_TOKEN'}, ) data = res.json()['data'] ``` **Response (watermark detected):** ```json theme={null} { "data": { "type": "watermarkExtract", "orderId": "123456789", "status": "complete", "statusCode": "detected", "watermarkText": "MORI_WATERMARK" } } ``` **Response (no watermark found):** ```json theme={null} { "data": { "type": "watermarkExtract", "orderId": "123456789", "status": "complete", "statusCode": "undetected" } } ``` Poll `status` to know when the job is done, then read `statusCode` for the outcome: | `status` | Meaning | | ------------ | ---------------------------- | | `pending` | Waiting for file upload | | `inProgress` | Extraction running | | `complete` | Finished — read `statusCode` | | `failed` | Extraction failed | | `statusCode` | Meaning | | ------------ | -------------------------------------------------------------- | | `detected` | Watermark found — check `watermarkText` for the extracted text | | `undetected` | No watermark detected in the image | ## Full React example Everything above, wired into one component: single-file upload with progress, backoff polling, and the extraction result rendered from `statusCode`. No dependencies beyond React. ```jsx WatermarkExtractChecker.jsx theme={null} import { useRef, useState } from 'react'; const API_BASE = 'https://api.bizmori.com/api/v2'; const API_TOKEN = import.meta.env.VITE_MORI_API_TOKEN; const ACCEPT_FORMATS = '.jpg,.jpeg,.png,.webp,.tiff,.bmp,.pdf'; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function api(path, { method = 'GET', body, idempotencyKey } = {}) { // Retries reuse the caller's idempotency key, so a retried create can never // produce a second order. Auth failures surface as `AUTH_*` codes — retrying // those will not help, so they fall through to the throw below. for (let attempt = 0; ; attempt++) { const res = await fetch(`${API_BASE}${path}`, { method, headers: { Authorization: `Bearer ${API_TOKEN}`, ...(body && { 'Content-Type': 'application/json' }), }, body: body && JSON.stringify({ idempotencyKey, ...body }), }); if (!res.ok) { const { code } = await res.json().catch(() => ({})); // 429 covers two cases: a transient rate limit, and PLAN_LIMIT_EXCEEDED, // which means the plan quota is spent and will never clear on retry. if (res.status === 429 && code !== 'PLAN_LIMIT_EXCEEDED' && attempt < 3) { await sleep(2 ** attempt * 1000); continue; } throw new Error(code ?? `HTTP_${res.status}`); } return (await res.json()).data; } } function putFile(file, uploadUrl, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', uploadUrl); xhr.setRequestHeader('Content-Type', file.type); xhr.upload.onprogress = (event) => { if (event.lengthComputable) onProgress(event.loaded / event.total); }; xhr.onload = () => xhr.status < 300 ? resolve() : reject(new Error(`UPLOAD_FAILED_${xhr.status}`)); xhr.onerror = () => reject(new Error('UPLOAD_NETWORK_ERROR')); xhr.send(file); }); } async function pollOrder(orderId, { onStatus, timeoutMs = 10 * 60 * 1000 } = {}) { const deadline = Date.now() + timeoutMs; let delay = 2000; while (Date.now() < deadline) { const order = await api(`/orders/${orderId}`); onStatus?.(order.status); if (['complete', 'failed', 'expired'].includes(order.status)) return order; await sleep(delay); delay = Math.min(delay * 1.5, 15000); } throw new Error('ORDER_POLL_TIMEOUT'); } export default function WatermarkExtractChecker() { const [file, setFile] = useState(null); const [progress, setProgress] = useState(0); const [status, setStatus] = useState('idle'); const [result, setResult] = useState(null); const [error, setError] = useState(null); const [orderId, setOrderId] = useState(null); const inFlight = useRef(false); const busy = status !== 'idle' && status !== 'complete'; async function checkWatermark(event) { event.preventDefault(); if (inFlight.current || !file) return; inFlight.current = true; // Snapshot the selection. The user can swap files while requests are in // flight, and `selected` has to stay paired with the order's upload URL. const selected = file; setError(null); setOrderId(null); setResult(null); setProgress(0); try { setStatus('creating order'); const order = await api('/orders/wtr-extract', { method: 'POST', idempotencyKey: crypto.randomUUID(), body: { file: { fileName: selected.name } }, }); setOrderId(order.orderId); setStatus('uploading'); await putFile(selected, order.file.uploadUrl, setProgress); const finished = await pollOrder(order.orderId, { onStatus: setStatus }); // `expired` is terminal too: the cleanup job drops the files after 7 days. if (finished.status !== 'complete') throw new Error(`ORDER_${finished.status.toUpperCase()}`); setResult(finished); setStatus('complete'); } catch (caught) { setError(caught.message); setStatus('idle'); } finally { inFlight.current = false; } } return (
{ setFile(event.target.files[0] ?? null); setError(null); setProgress(0); setResult(null); }} /> {status !== 'idle' &&

Status: {status}

} {status === 'uploading' &&

Uploading: {Math.round(progress * 100)}%

} {/* `status` says the job finished; `statusCode` says what was found. */} {result?.statusCode === 'detected' && (

Watermark detected: {result.watermarkText}

)} {result?.statusCode === 'undetected' &&

No watermark was found in this image.

} {error &&

Failed: {error}{orderId && ` (orderId: ${orderId})`}

}
); } ``` `import.meta.env.VITE_MORI_API_TOKEN` is Vite's syntax. Use `process.env.NEXT_PUBLIC_MORI_API_TOKEN` on Next.js, or whatever your bundler exposes to client code. `crypto.randomUUID()` needs a secure context: HTTPS and `localhost` are fine, a plain-HTTP LAN address is not. The public result contains `statusCode` and, when detected, `watermarkText`; it never includes a MID. ## Error handling | HTTP Status | Meaning | Action | | ----------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `400` | Invalid request | Check parameters and file format | | `401` | Authentication failed | Verify your API key | | `429` | Rate limit, or `PLAN_LIMIT_EXCEEDED` when the plan quota is spent | Check the `code` field. A rate limit clears on retry; `PLAN_LIMIT_EXCEEDED` does not — upgrade the plan | For the complete error code reference, see [Error Codes](/errors). ## Next steps Protect images from AI training and generation. Embed invisible watermarks into images. Detect AI-generated images with probability scores. Set up webhooks to get notified when processing completes. # Test API keys Source: https://docs.bizmori.com/test-api-keys Exercise the BIZ MORI order lifecycle without processing files or consuming credits. Use the automatically issued test API key with the `sk_test_` prefix to validate your integration before you use a live API key. Test API keys use the same Bearer authentication as live keys, but they do not run Anti-AI, watermark, or AI Detection processing. They also do not create result files or consume customer credits. Persistent test orders and test webhooks are enabled per environment. If your environment returns synthetic orders that are not retained, it is using the legacy mock behavior. Use a live API key when you need to evaluate processing quality or download a result file. ## Test order lifecycle Call the same order creation endpoint that you use for a live integration. Send the test key in the `Authorization` header and provide an `idempotencyKey`. Generate a new UUIDv4 for each logical order, and reuse it only when retrying that same request after a network failure. ```bash theme={null} curl -X POST https://api.bizmori.com/api/v2/orders/anti-ai \ -H "Authorization: Bearer $BIZMORI_TEST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "idempotencyKey": "2f1c3e4a-8b57-4a6d-9c20-7e5f1a3b6d42", "files": [{"fileName": "sample.png"}] }' ``` For an upload-mode test order, send the file to the `uploadUrl` returned by the order response. A test upload URL uses the `PUT /api/v2/test-uploads/{token}` route. The upload URL carries its own authorization. Do not add an API key header. BIZ MORI consumes the request stream, records upload completion, and discards the file contents. ```bash theme={null} curl -X PUT "TEST_UPLOAD_URL" \ --data-binary @sample.png ``` The default test upload limit is 50 MiB. Use [Refresh presigned URLs](/api-reference/orders/refresh-urls) when an upload URL expires and the order is still waiting for an upload. Call the confirm endpoint after the upload for Anti-AI upload mode and AI Detection. Watermark Embed and Watermark Extract start after their required uploads finish. Anti-AI URL mode starts when you create the order. Test orders follow the same state shape as live orders: `pending → inProgress → complete` or `failed` The simulated processing delay is about five seconds. Poll [Get order](/api-reference/orders/get-order), or use a test webhook endpoint when webhooks are enabled in your environment. Use [List orders](/api-reference/orders/list-orders), [Recent usage statistics](/api-reference/orders/recent-stats), and [Get order details](/api-reference/orders/get-order) with the same test key. You see only test orders owned by the account associated with that key. ## Service flows | Service | Create | Upload | Confirm | | ------------------- | ---------------------------------- | --------------- | ------------ | | Anti-AI upload mode | `POST /api/v2/orders/anti-ai` | Test upload URL | Required | | Anti-AI URL mode | `POST /api/v2/orders/anti-ai` | Not required | Not required | | Watermark Embed | `POST /api/v2/orders/wtr-embed` | Test upload URL | Not required | | Watermark Extract | `POST /api/v2/orders/wtr-extract` | Test upload URL | Not required | | AI Detection | `POST /api/v2/orders/ai-detection` | Test upload URL | Required | Do not call a confirm endpoint for Watermark Embed or Watermark Extract. The request will fail because those services start after their uploads complete. ## Choose a deterministic result Use the input file name to exercise success and failure states without depending on an external processing service. | File name suffix | Result | | -------------------- | ---------------------------------------------- | | `*_fail.` | The order fails | | `*_detected.` | Watermark Extract reports a detected watermark | | `*_undetected.` | Watermark Extract reports no watermark | | `*_ai.` | AI Detection returns probability `0.98` | | `*_human.` | AI Detection returns probability `0.02` | Without a suffix, Anti-AI and Watermark Embed complete successfully, Watermark Extract reports no watermark, and AI Detection returns probability `0.02`. If any file in a multi-file order uses `_fail`, the order fails. ## Test webhooks Create an owned test webhook endpoint with the Dashboard, a live API key, or a test API key. A test key may omit `isTest` or set it to `true`; `isTest: false` returns `403 AUTH_FORBIDDEN`: ```bash theme={null} curl -X POST https://api.bizmori.com/api/v2/orders/webhooks \ -H "Authorization: Bearer $BIZMORI_TEST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Integration test webhook", "url": "https://example.com/test-webhook", "isTest": true }' ``` * A test API key can create, list, get, update, and delete test endpoints owned by the same owner, list their events, and retry failed events. Live-mode and other-owner resources remain hidden. * `isTest: true` endpoints receive test order events only. * `isTest: false` endpoints receive live order events only. * Test webhook payloads use the same event names, HMAC-SHA256 signature, and retry behavior as live webhooks. * Anti-AI and Watermark Embed test events use `downloadUrl: null` because no result file is created. See [Webhooks](/webhooks) for owner and mode isolation, signature verification, event payloads, and retry rules. ## API behavior and limitations | Operation | Test API key behavior | | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET /api/v2/orders` | Returns the account's persistent test orders | | `GET /api/v2/orders/stats/recent` | Returns statistics for the account's test orders | | `GET /api/v2/orders/{orderId}` | Returns a test order owned by the key's account | | `POST /api/v2/orders/{orderId}/refresh-urls` | Refreshes an upload URL that has not been used yet | | `GET /api/v2/orders/{orderId}/download` | Returns `404 PROCESSED_FILE_NOT_FOUND` | | Webhook management and retries | Manage only test endpoints owned by the same owner; live-mode and other-owner IDs return `404`, while `isTest: false` returns `403 AUTH_FORBIDDEN` | Test orders remain separate from live orders. A test key cannot read or modify live orders, and a live key cannot read or modify test orders. Test API keys do not: * run image or document processing; * reserve or consume credits; * create downloadable result files; or * send events to live webhook endpoints. For authentication details, see [Authentication](/authentication). For error handling, see [Error Codes](/errors). # Webhooks Source: https://docs.bizmori.com/webhooks Receive real-time push notifications when orders complete or fail Webhooks deliver HTTP `POST` callbacks to your server when order processing completes or fails. Use webhooks instead of polling the [Get Order](/api-reference/orders/get-order) endpoint — your server gets notified the moment a result is ready. ## Setting up webhooks You can create a webhook endpoint via the API or the [BIZ MORI Dashboard](https://app.bizmori.com/webhooks). The example uses a live API key and creates a live endpoint. To create one via the API, use the [Create Webhook](/api-reference/webhooks/create-webhook) endpoint: ```bash theme={null} curl -X POST https://api.bizmori.com/api/v2/orders/webhooks \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "My Webhook", "url": "https://your-server.com/webhook" }' ``` **Response:** ```json theme={null} { "data": { "id": 1, "name": "My Webhook", "secret": "whsec_xxxxxxxxxxxxxxxxxx" } } ``` Save the `secret` immediately — **it is shown only once** and cannot be retrieved again. You need it to verify every incoming webhook signature. Your webhook handler must: * Accept `POST` requests with a JSON body * Respond with a `2xx` status code within **5 seconds** * Verify the `X-MoriBiz-Signature` header before processing Every request includes an `X-MoriBiz-Signature` header. See [Signature Verification](#signature-verification) below for implementation in your language. ## Testing webhooks To receive test-order events, register a separate test endpoint from the Dashboard, a live API key, or a test API key. A test key may omit `isTest` or set it to `true`; `isTest: false` returns `AUTH_FORBIDDEN`: ```bash theme={null} curl -X POST https://api.bizmori.com/api/v2/orders/webhooks \ -H "Authorization: Bearer $BIZMORI_TEST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Integration test webhook", "url": "https://example.com/test-webhook", "isTest": true }' ``` * A test API key can create, list, get, update, and delete test endpoints owned by the same owner, list their events, and retry failed events. Live-mode and other-owner resources remain hidden. * An `isTest: true` endpoint receives test order events only. * An `isTest: false` endpoint receives live order events only. * Test webhook payloads use the same event names, HMAC-SHA256 signature, timeout, and retry behavior as live webhooks. * Anti-AI and Watermark Embed test events use `downloadUrl: null` because test processing does not create a result file. Test endpoint CRUD, event listing, and retry operations use the same owner and test-mode isolation described below. ## Event types | Event Type | Description | | ---------------------------------- | ------------------------------------------- | | `order.antiAi.completed` | Anti-AI processing completed successfully | | `order.antiAi.failed` | Anti-AI processing failed | | `order.watermarkEmbed.completed` | Watermark embedding completed successfully | | `order.watermarkEmbed.failed` | Watermark embedding failed | | `order.watermarkExtract.completed` | Watermark extraction completed successfully | | `order.watermarkExtract.failed` | Watermark extraction failed | | `order.aiDetection.completed` | AI Detection completed successfully | | `order.aiDetection.failed` | AI Detection failed | ## Webhook payload All webhook payloads follow this structure: ```json theme={null} { "eventId": "550e8400-e29b-41d4-a716-446655440000", "eventType": "order.antiAi.completed", "occurredAt": "2026-02-19T12:00:00.000Z", "data": { "orderId": "123456789", "orderName": "anti_ai_2026-02-19", "createdAt": "2026-02-19T11:50:00.000Z", "updatedAt": "2026-02-19T12:00:00.000Z", "completedAt": "2026-02-19T12:00:00.000Z", "status": "complete" // ...service-specific fields } } ``` ### Completed event — common fields | Field | Type | Description | | ------------- | ------ | ------------------------------------- | | `orderId` | string | Order ID | | `orderName` | string | Order name | | `createdAt` | string | Order creation time (ISO 8601) | | `updatedAt` | string | Order last updated time (ISO 8601) | | `completedAt` | string | Processing completion time (ISO 8601) | | `status` | string | Always `complete` | ### Failed event — common fields | Field | Type | Description | | -------------- | ------ | ---------------------------------- | | `orderId` | string | Order ID | | `orderName` | string | Order name | | `createdAt` | string | Order creation time (ISO 8601) | | `updatedAt` | string | Order last updated time (ISO 8601) | | `failedAt` | string | Processing failure time (ISO 8601) | | `status` | string | Always `failed` | | `errorCode` | string | Error code | | `errorMessage` | string | Error message | ### Anti-AI / Watermark Embed — additional fields (completed) | Field | Type | Description | | ------------- | -------------- | ----------------------------------------------------------------------------------- | | `fileCount` | integer | Number of processed files | | `downloadUrl` | string \| null | Result file download URL (valid for 7 days for live orders; `null` for test orders) | ### Watermark Extract — additional fields (completed) The `status` field is always `complete`. Use `statusCode` to determine the detection result. | Field | Type | Description | | -------------------- | ------- | -------------------------------------------------------------- | | `statusCode` | string | `detected` or `undetected` | | `watermarkFound` | boolean | Whether a watermark was detected | | `watermarkInfo.text` | string | Detected watermark text (only when `watermarkFound` is `true`) | The payload exposes text only: it never includes MID, `midDecimal`, or `midHex`. ### AI Detection — additional fields (completed) | Field | Type | Description | | ------------- | -------------- | -------------------------------------------------------------------------------------------- | | `probability` | number | Probability of being AI-generated (0.0–1.0) | | `heatmapUrl` | string \| null | AI detection heatmap image download URL (only when `generateHeatmap` option was used) | | `overlayUrl` | string \| null | Heatmap overlay on original image download URL (only when `generateOverlay` option was used) | ## Signature verification Verify webhook authenticity by checking the `X-MoriBiz-Signature` header. Always verify **before** processing the event. ```javascript Node.js theme={null} const crypto = require('crypto'); function verifyWebhookSignature(rawBody, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(rawBody) // use the raw request body string .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } // Express handler app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-moribiz-signature']; if (!verifyWebhookSignature(req.body.toString(), signature, process.env.WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } const { eventType, data } = JSON.parse(req.body); console.log(`Received ${eventType} for order ${data.orderId}`); res.sendStatus(200); }); ``` ```python Python theme={null} import hmac import hashlib def verify_webhook_signature(raw_body: str, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), raw_body.encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected) # Flask handler from flask import Flask, request, abort import json app = Flask(__name__) @app.route('/webhook', methods=['POST']) def webhook(): signature = request.headers.get('X-MoriBiz-Signature') raw_body = request.get_data(as_text=True) if not verify_webhook_signature(raw_body, signature, os.environ['WEBHOOK_SECRET']): abort(401) event = json.loads(raw_body) return '', 200 ``` ```php PHP theme={null} handleWebhook( @RequestBody String rawBody, @RequestHeader("X-MoriBiz-Signature") String signature) throws Exception { if (!verifySignature(rawBody, signature, webhookSecret)) { return ResponseEntity.status(401).build(); } // process event... return ResponseEntity.ok().build(); } ``` ## Retry policy BIZ MORI uses a multi-layered retry system to ensure reliable webhook delivery. ### Initial retry If your endpoint doesn't return a `2xx` status code, BIZ MORI immediately retries: | Attempt | Delay | | --------- | --------- | | 1st retry | 1 second | | 2nd retry | 2 seconds | | 3rd retry | 4 seconds | ### Scheduler-based retry If all initial retries fail, the event enters a scheduler-based retry queue with increasing intervals: | Attempt | Delay | | --------- | ---------- | | 4th retry | 30 minutes | | 5th retry | 2 hours | | 6th retry | 4 hours | After all scheduler-based retries are exhausted, the event is marked as `FAILED`. You can view failed events using the [List Webhook Events](/api-reference/webhooks/list-events) endpoint. ### Manual resend API Regardless of the automatic retry process, you can manually resend webhooks at any time using the resend APIs: * **Single event resend**: Retry a specific failed event using the [Retry Webhook Event](/api-reference/webhooks/retry-event) endpoint. * **Bulk resend**: Retry all failed events within a date range (up to 7 days) using the [Retry Failed Webhook Events](/api-reference/webhooks/retry-failed-events) endpoint. ```bash theme={null} # Retry a single event curl -X POST https://api.bizmori.com/api/v2/orders/webhooks/{webhookId}/events/{eventId}/retry \ -H "Authorization: Bearer YOUR_API_TOKEN" # Retry all failed events in a date range curl -X POST https://api.bizmori.com/api/v2/orders/webhooks/{webhookId}/events/retry-failed \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "fromDate": "2026-01-01", "toDate": "2026-01-07" }' ``` ### Failure notification email When initial webhook delivery fails, BIZ MORI sends a failure notification email to the account owner. This email is sent at most **once per day** to avoid excessive notifications. ## Managing webhooks ## Test API key scope A test API key can manage only test endpoints owned by the same owner. It can list, get, create, update, and delete those endpoints; list their events; retry one failed event; and retry failed events in a date range of at most seven days. Event responses do not add delivery-log response bodies. * On create, omitting `isTest` or setting it to `true` creates a test endpoint. Setting `isTest: false` returns `AUTH_FORBIDDEN`. * On update, omitting `isTest` keeps test mode and `true` is allowed. Setting `isTest: false` returns `AUTH_FORBIDDEN`. * A live endpoint ID or another owner's endpoint ID returns `WEBHOOK_NOT_FOUND`; this does not disclose whether that endpoint exists. * An event ID outside an accessible endpoint returns `WEBHOOK_EVENT_NOT_FOUND`. Live API keys and the Dashboard keep their existing ability to choose `isTest` and manage the owner's live and test endpoints. Secret one-time display, URL validation, HMAC signatures, and the retry policy are unchanged. | Action | Endpoint | | -------------------- | ---------------------------------------------------------------------------------- | | List all webhooks | [GET /webhooks](/api-reference/webhooks/list-webhooks) | | Create a webhook | [POST /webhooks](/api-reference/webhooks/create-webhook) | | Get webhook details | [GET /webhooks/](/api-reference/webhooks/get-webhook) | | Update a webhook | [PUT /webhooks/](/api-reference/webhooks/update-webhook) | | Delete a webhook | [DELETE /webhooks/](/api-reference/webhooks/delete-webhook) | | View delivery events | [GET /webhooks//events](/api-reference/webhooks/list-events) | | Retry a single event | [POST /webhooks//events//retry](/api-reference/webhooks/retry-event) | | Retry failed events | [POST /webhooks//events/retry-failed](/api-reference/webhooks/retry-failed-events) |