# Twinbly for Developers

Turn photos, video, and capture data into hosted, streamable Gaussian splats through one API.

Build capture processing into your application without operating a reconstruction pipeline. Upload a capture, start processing, follow its progress, and retrieve the resulting assets. Processing runs asynchronously; your application can keep working while the job runs.

This guide covers the processing API. It does not require a Twinbly CLI, SDK, or MCP server. Those tools are not currently part of this guide's supported distribution.

## Start here

The processing API base URL is `https://rg-splats.web.app/v1`. The service name in the address is historical; this is the Twinbly processing service.

- [Developer console](https://www.twinbly.com/dashboard/developers) — signed-in processing history and diagnostics
- [Processing API schema](https://rg-splats.web.app/v1/openapi.json)
- [Capture guide](https://www.twinbly.com/learn/capture-guide)
- [Twinbly pricing](https://www.twinbly.com/pricing)

The schema includes some fields and operations marked `x-status: planned`. Do not build a production dependency on those entries. Examples in this guide use the existing upload-and-start workflow.

New integration? [Request processing access](mailto:hello@twinbly.com?subject=Twinbly%20API%20integration) with your use case and expected capture volume. Processing onboarding is assisted while we verify account and billing linkage; the public API is available to provisioned integrations.

## Authentication

Backend integrations send a processing key in the `X-API-Key` header. Store the key on your server or in a local environment variable; do not put it in a browser bundle, native application binary, repository, or shared prompt.

Processing keys and scene access tokens are different credentials. The `tbly_` read-only tokens in account settings do not authorize processing. Processing keys use the `rgs_` family. Existing partners should use their provisioned processing credential.

Processing-key management is available through `/v1/me/keys` using a signed-in user's Firebase ID token as `Authorization: Bearer …`. An API key cannot create other API keys. A new key's secret is returned once. The dashboard's read-only token controls are not a processing-key onboarding screen.

First-party native integrations use their existing Firebase identity flow. Refresh the ID token for each request rather than capturing one token before a long polling loop. Third-party keys act for their own account; omit `userId` rather than choosing another user's identity.

Before starting paid work, confirm the credential's billing account with your integration contact. This guide does not promise a hard per-job spending cap. Quotes estimate cost; a null balance or affordability field means unknown, not free processing or permission to spend.

## Quote a capture

Set `TWINBLY_API_KEY` in your environment without committing its value. These examples use curl and jq. Do not enable shell tracing around credentials or upload/download URLs.

```bash
curl --fail-with-body --silent --show-error \
  'https://rg-splats.web.app/v1/jobs/quote' \
  -H "X-API-Key: ${TWINBLY_API_KEY}" \
  -H 'Content-Type: application/json' \
  --data '{"frames":120,"width":1920,"height":1080,"inputType":"zip","recipe":{"engine":"lichtfeld","iterations":30000,"targetMegapixels":"preserve","useGut":false}}'
```

Use your actual image count and dimensions. For video, quote the intended extracted keyframes rather than every encoded video frame. Check `feasible` and reported limits first, then `estimate.cost`, any estimate range, and `adjustments`. A capture beyond a processing limit will not become feasible by adding money.

You can also validate a creation request by sending the same body to `POST /v1/jobs` with the boolean `dryRun: true`. This does not create or start a job. An estimate is not a guaranteed final charge or processing time.

## Create and upload

Prepare a ZIP of overlapping photos from one capture. This first example assumes you have already checked the estimate and agreed to the intended processing cost. The recipe below is illustrative; inspect the dry-run response for routing and adjustments before accepting it.

Choose a `captureId` unique to this intended job and save it with your application record. Keep it stable during recovery. Do not automatically retry a creation request with a new identifier.

```bash
umask 077
curl --fail-with-body --silent --show-error \
  'https://rg-splats.web.app/v1/jobs' \
  -H "X-API-Key: ${TWINBLY_API_KEY}" \
  -H 'Content-Type: application/json' \
  --data '{"captureId":"replace-with-your-unique-capture-id","inputType":"zip","recipe":{"engine":"lichtfeld","iterations":30000,"targetMegapixels":"preserve","useGut":false},"analysis":{"schemaVersion":2,"capture":{"actualFrameCount":120}}}' \
  --output twinbly-job.json
```

Use the actual frame count, consistent with your quote. Check the request succeeded before reading the response. Save `jobId` durably before proceeding. Treat the response file as private: it contains an upload capability. It can also include `adjustments`; review these before starting.

The response's `upload` describes the destination, method, content type, and required headers. Upload the file bytes to that destination, not to `/v1/jobs`. Do not send your API key to the storage URL. Prefer the returned resumable session for large uploads. Only call `/start` after storage confirms the complete upload.

For a single PUT, use the method and all headers returned by the service, including its exact Content-Type. Do not substitute `multipart/form-data` or guess the MIME type from your local filename. If the target expires, follow upload recovery for the existing job with your integration contact; creating and starting another job is not an upload-resume strategy.

### Recover a lost creation response

Look up your original capture before attempting another create:

```bash
curl --fail-with-body --silent --show-error --get \
  'https://rg-splats.web.app/v1/jobs/list' \
  -H "X-API-Key: ${TWINBLY_API_KEY}" \
  --data-urlencode 'captureId=replace-with-your-unique-capture-id'
```

The current creation contract does not guarantee replay of the same job throughout its lifecycle. In particular, a repeat create after processing starts can produce another job. If recovery is ambiguous, stop and reconcile the existing job rather than automatically creating billable work. Do not treat the Idempotency-Key header alone as a universal retry guarantee.

## Start and monitor

Starting processing is the paid-work boundary. Persist the job ID and the caller's spending authorization before this step.

```bash
TWINBLY_JOB_ID=$(jq -er '.jobId | select(type == "string" and length > 0)' twinbly-job.json)
curl --fail-with-body --silent --show-error --request POST \
  "https://rg-splats.web.app/v1/jobs/${TWINBLY_JOB_ID}/start" \
  -H "X-API-Key: ${TWINBLY_API_KEY}"
```

Read status using `GET /v1/jobs/{jobId}`. Poll with backoff; honor `Retry-After` when supplied. If a start response is lost, inspect the same job before issuing another start. Starting a failed job can rerun processing and should be an intentional decision.

Training `complete` is not a guarantee that every converted delivery format is ready. Read the job's available status information and its downloads response for the format your application needs. Handle failure explicitly rather than polling forever. Treat unknown statuses as unsupported pending states and surface them for investigation; do not assume success.

Callbacks are available to configured integrations, but planned retry schedules and self-service signing-secret operations must not be assumed live. Keep polling as a recovery path. Do not treat an unverified webhook body as authority to fetch arbitrary URLs or update someone else's job.

## Retrieve and use results

```bash
curl --fail-with-body --silent --show-error \
  "https://rg-splats.web.app/v1/jobs/${TWINBLY_JOB_ID}/downloads" \
  -H "X-API-Key: ${TWINBLY_API_KEY}" \
  --output twinbly-downloads.json
```

Use this authenticated downloads endpoint to obtain fetchable asset URLs. A `gs://` path in a job record is a storage identifier, not a public HTTP download. Signed URLs expire and should not be logged or committed. Request fresh download links when needed.

Some converted results include hosted delivery URLs in the receipt. Use the URLs returned by the service rather than constructing paths from a job ID. Formats and readiness vary; do not assume PLY, SOG/SOGS, and 3D Tiles are all present on every job. Preserve the relative sibling-file resolution of multi-file streaming assets.

A hosted asset is different from a published Twinbly scene. A job may have no `editorUrl` or `viewerUrl`; library ownership and publishing require the relevant integration workflow. Do not promise that raw API processing automatically creates a public Twinbly page. Publish only when the user intends to share.

Runtime clipping and editing do not inherently redact exported source assets. Confirm the export behavior for your integration before distributing files that contain sensitive geometry. Hosted asset links also have their own access and retention policy; API-key revocation is not a promise to invalidate already issued asset URLs.

## Input choices

| Input | Use |
| --- | --- |
| `zip` | A photo capture packaged as a ZIP |
| `video` / `mov` | Video capture; processing extracts frames |
| `360` | Supported equirectangular 360 input; not a blanket promise for every camera's proprietary files |
| `colmap` | Prepared imagery and reconstruction; follow the schema's bundle layout |
| `capture` | Prepared device capture bundle; follow the schema's bundle layout |

Use the current schema for exact bundle requirements and processing constraints. Do not send the planned `inputSource: manifest` field expecting remote-file ingestion. Survey/megasplat workflows and proprietary camera preflight flows require their own integration guidance.

## Activity and diagnostics

The [developer console](https://www.twinbly.com/dashboard/developers) shows the latest 100 dated jobs associated with your Twinbly web, native, and self-service API identity. Search by job ID or status and filter by source. Copy details includes only the job ID, source, status, and input type. Custom partner tenants can use a separate identity and are not automatically joined to your account. This is job history, not a request-by-request access log.

For credential-scoped reporting, use `GET /v1/usage?view=activity`. It counts every job state, with separate estimated and recorded actual processing costs. Missing cost data is null, not zero; coverage counts tell you how many jobs contributed to a subtotal. These costs are not wallet charges. Use Twinbly account settings for billing history.

The default usage response retains its completed-only view. Both views now confine non-delegating credentials to their own qualified subject; requesting another user returns 403. Reports exceeding 5,000 matching records are refused rather than silently truncated. Date filtering happens after that cap; date-only end dates mean midnight UTC. Use project scope to narrow large reports.

## Reference and troubleshooting

| Operation | Purpose |
| --- | --- |
| `GET /v1/me` | Inspect the authenticated processing account |
| `GET /v1/gpu-catalog` | Read available processing capabilities |
| `POST /v1/jobs/quote` | Estimate feasibility and cost |
| `POST /v1/jobs` | Validate with dryRun, or reserve a job and upload target |
| `GET /v1/jobs/list` | List visible jobs or recover by captureId |
| `POST /v1/jobs/{jobId}/start` | Start uploaded input |
| `GET /v1/jobs/{jobId}` | Read job status |
| `GET /v1/jobs/{jobId}/downloads` | Retrieve result links and delivery information |
| `POST /v1/jobs/{jobId}/cancel` | Request cancellation; inspect the result rather than assuming immediate interruption or no charge |
| `POST /v1/jobs/{jobId}/convert` | Request another delivery conversion; availability and limits apply |
| `GET /v1/usage` | Read usage information |

For 401, check credential type and freshness. For 403, verify ownership instead of changing user IDs. For 429, honor the retry guidance. For a timeout during a write, reconcile the existing job before retrying. Retain the job ID, HTTP status, and machine-readable error code for support; redact credentials and signed URLs.

## Using Twinbly from an agent

Give the agent this guide and the schema, with credentials supplied through its authorized runtime rather than prompt text. The agent should quote first, obtain the user's spending decision, persist the job handle, upload, start once, monitor, and return available output links. Creation, cancellation, reruns, conversion, and publication are separate decisions.

There is no required agent framework. A coding agent can use HTTP directly today. Do not invent a Twinbly package installation command or MCP connection address: supported distributions will be documented here when available.
