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

# Portfolio Intake

> How TattooAPI stages artist SMS/MMS portfolio media before review, classification, and approved portfolio projection.

# Portfolio Intake

Portfolio intake is the first governed workflow for artists texting portfolio work, flash, references, and ideas to a managed number.

The product goal is personal for artists and strict for the platform: inbound media can be staged, classified, reviewed, and promoted into approved portfolio storage, but it cannot jump straight from a text message into public distribution.

<Warning>
  Communications API credentials can be ready before SMS/MMS launch is ready. A2P approval remains required before treating SMS/MMS intake as production-ready.
</Warning>

<Note>
  Review-gated surface: this workflow can stage evidence and prepare review records, but it cannot publish, promote, or write approved portfolio data without TattooAPI policy checks and human approval.
</Note>

<Warning>
  Private workers cannot write directly to approved portfolio storage, mutate canonical runtime tables, activate public surfaces, change identity mappings, or publish inbound media. They return classification and review evidence to TattooAPI.
</Warning>

## Flow

```text theme={"dark"}
Artist SMS/MMS
  -> managed number and conversation inbox
  -> TattooAPI webhook and policy boundary
  -> private object storage for raw/staged media
  -> upload integrity completion gate
  -> private worker classification
  -> human review
  -> approved portfolio storage
  -> TattooAPI projections to Artist OS, Studio OS, tattoo.co, websites, and SDKs
```

## System Roles

| System                     | Role                                                                                       | Authority                               |
| -------------------------- | ------------------------------------------------------------------------------------------ | --------------------------------------- |
| Communications buffer      | SMS/MMS buffer, inbox, conversation history, contact context                               | Evidence source only                    |
| TattooAPI                  | Identity, policy, staging, review, audit, projection, and write boundary                   | Governing authority                     |
| Object storage             | Temporary private media staging and lifecycle retention                                    | Evidence storage only                   |
| Private classifier         | Private classification worker for category, tag, caption, quality, and blocker suggestions | Review evidence only                    |
| Private workflow tooling   | Workflow orchestration, retries, and review packaging                                      | Workflow layer only                     |
| Approved portfolio storage | Approved portfolio and editorial media lake after review                                   | Storage destination after approval      |
| TattooAPI runtime          | Runtime truth after governed promotion                                                     | Canonical only after explicit promotion |

## Intake Contract

Inbound portfolio messages should normalize into a staging-only envelope:

```ts theme={"dark"}
type PortfolioIntakeMessage = {
  provider: "communications_provider";
  providerLocationId: string;
  providerConversationId: string;
  providerMessageId: string;
  providerContactId: string;
  fromPhoneHash: string;
  text?: string;
  media: PortfolioIntakeMedia[];
  identityStatus: "unmatched" | "phone_matched" | "verified_by_operator" | "review_required";
  writeMode: "staging_only";
};
```

Raw phone numbers, private bucket URLs, communications provider tokens, approved storage tokens, and worker API keys must not appear in committed examples, docs, reports, screenshots, or prompts.

## Partner API Staging

Permanent and other approved private services use a WorkOS organization API key with `portfolio_intake:stage`. The current Production Permanent key has exactly `artists:read` and `portfolio_intake:stage`; `artists:read` is used to inspect existing artist identity evidence before staging.

```bash theme={"dark"}
curl -X POST "https://api.tattooapi.com/api/private/portfolio-intake/v1/submissions" \
  -H "X-API-Key: $TATTOO_WORKOS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotencyKey": "opaque-message-id:opaque-attachment-id",
    "tenantId": "studio_canonical_id",
    "studioCanonicalId": "studio_canonical_id",
    "artistCanonicalId": "artist_canonical_id",
    "source": "permanent_imessage",
    "sourceMessageId": "opaque-message-id",
    "fileName": "portfolio.jpg",
    "mimeType": "image/jpeg",
    "sizeBytes": 1048576,
    "rightsPosture": "review_required",
    "consentPosture": "review_required"
  }'
```

TattooAPI returns a five-minute private R2 `PUT` URL and an `awaiting_upload` submission. Upload the exact media bytes using the returned `Content-Type` header. By default, the API key mapping must already include the requested studio or artist canonical ID; cross-tenant submissions return `403`.

An explicitly approved service mapping may carry the internal `portfolio_intake:stage_cross_tenant` capability for governed identity resolution and staging across approved tenants. This capability belongs to TattooAPI's Convex actor mapping, not to the WorkOS key, and never grants approval or publication authority.

While a submission remains `awaiting_upload`, legitimate idempotent replay returns the existing submission and a new signed URL for that submission's persisted object key. TattooAPI never re-signs a PUT after completion. Reusing an idempotency key with a different semantic envelope fails closed instead of changing the staged pointer.

## Complete the Upload

After the `PUT` succeeds, call the bodyless completion route with the same WorkOS organization API key and `portfolio_intake:stage` permission:

```bash theme={"dark"}
curl -X POST \
  "https://api.tattooapi.com/api/private/portfolio-intake/v1/submissions/$SUBMISSION_ID/upload-complete" \
  -H "X-API-Key: $TATTOO_WORKOS_API_KEY"
```

This route accepts `X-API-Key` only. Bearer-only requests and requests that mix `Authorization` with `X-API-Key` are rejected before submission lookup or storage verification.

TattooAPI reads the private R2 object, checks its image signature and R2 MIME metadata, compares its actual byte count with the original submission, computes and persists a server-side SHA-256 digest, and requires an opaque ETag. Only `image/jpeg`, `image/png`, `image/webp`, and `image/heic` are accepted. `image/heif` is not supported.

The first valid completion atomically transitions `awaiting_upload` to `awaiting_review` and returns `action: "completed"`. A later replay returns HTTP `200` with `action: "already_completed"` from the persisted verification evidence. Missing objects, size or MIME mismatch, integrity conflict, or any other intake state leave the submission unadvanced.

Completion is a staging transition only. Its governance envelope remains:

```json theme={"dark"}
{
  "reviewRequired": true,
  "directSanityWriteAllowed": false,
  "publicSurfaceActivated": false,
  "canonicalConvexMutationExecuted": false
}
```

The submission remains `review_required`. A different operator identity with `portfolio_intake:review` must approve it through:

```text theme={"dark"}
POST /api/private/portfolio-intake/v1/submissions/{submissionId}/approve
```

Approval is available only after upload completion has produced `awaiting_review`. It requires non-review rights and consent posture, verifies that the private R2 object exists, uploads the image to the approved Sanity media lake, and creates a private `tattooPortfolioAsset` draft. It does not activate public distribution or canonical Convex runtime promotion.

The submitting service can poll its own bounded status through:

```text theme={"dark"}
GET /api/private/portfolio-intake/v1/submissions/{submissionId}
```

The response can move through `awaiting_upload`, `awaiting_review`, `promotion_failed`, and `promoted_to_sanity`. Even `promoted_to_sanity` does not make the asset public; public reads require a separate governed runtime and distribution promotion.

## Identity Evidence, Not KYC

TattooAPI v0 identity evidence helps route portfolio media to review. It is not legal KYC and does not claim government identity verification.

| Status                 | Meaning                                                                     | What happens next                                                        |
| ---------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `unmatched`            | No reliable contact, phone hash, or profile mapping was found.              | Keep the item in review.                                                 |
| `phone_matched`        | A normalized phone hash or contact record matched known TattooAPI evidence. | Review can use the match as evidence, not final proof.                   |
| `review_required`      | Evidence is partial, conflicting, or below confidence threshold.            | Operator review is required before any approved write.                   |
| `verified_by_operator` | A human operator approved the identity evidence for this workflow.          | The item can proceed to approved workflow steps if all other gates pass. |

Do not label a record `kyc_verified` unless a future legal identity provider is explicitly added and approved.

## Classification Contract

Workers return suggestions, not mutations:

```ts theme={"dark"}
type PortfolioClassificationResult = {
  intakeId: string;
  classification:
    | "portfolio_work"
    | "flash_available"
    | "reference_idea"
    | "in_progress"
    | "duplicate_candidate"
    | "low_quality"
    | "needs_review";
  confidence: number;
  suggestedTags: string[];
  suggestedStyles?: string[];
  suggestedColors?: string[];
  suggestedPlacement?: string;
  suggestedSize?: string;
  suggestedAltText?: string;
  suggestedCaption?: string;
  reviewRequired: true;
  forbiddenActionsConfirmed: [
    "write_approved_storage",
    "publish_public",
    "mutate_identity",
    "promote_runtime"
  ];
};
```

## Public Surface

The public developer surface should expose only reviewed projections and public-safe contracts:

* approved portfolio reads
* public artist/studio relationships
* sanitized portfolio metadata
* review and ownership posture fields
* status-aware API documentation

It should not expose:

* raw inbound SMS/MMS payloads
* private conversation history
* raw media staging URLs
* direct approved-storage write paths
* worker credentials or host setup secrets
* automatic public publishing from inbound messages

<Tip>
  Data posture legend: `public_profile` can appear in public projections, `private_raw` stays internal, `sanitized_metric` can feed signals, `ontology_fact` can enter the governed graph, and `secret_reference` never leaves host secrets or private runtime configuration.
</Tip>
