# Conversions

Create a conversion, read it, list them, unlock one, follow its progress.

### Create a conversion

`POST /v1/conversions`

Converting is FREE: 1 credit = 1 image, spent when the image is UNLOCKED (here with `unlock: true`, or later with POST /conversions/{id}/unlock). A free conversion counts against the daily caps (guests 3 a UTC day, free accounts 10, customers 100). A guest never unlocks: `unlock: true` answers it 403 `account_required`. Accepted formats: PNG, JPG, WebP, AVIF, GIF, BMP and TIFF (read by their bytes; an animation or a multi-page TIFF at its first frame), at most 4096×4096. HEIC: send a PNG (the website converts it in the browser). Repeating an `idempotencyKey` answers 200 with the conversion it already made, never a second one and never a second charge.

| Field | In | Type | Required | What |
| --- | --- | --- | --- | --- |
| `Retry-After` | header | string | no | On a 429 (`free_limit_reached`, or every free conversion together reaching today's shared capacity of 2000): seconds until the caps renew, the next UTC midnight. On a 409 `too_many_active_jobs` (guest 1, free 1, paid 5 at once): wait for one to end, or send `queue: true`. |
| `inputKey` | body | string | yes | From POST /uploads. Must be the caller's. |
| `inputHash` | body | string | no | OPTIONAL assertion about what was uploaded. The API computes the real hash itself and answers `input_hash_mismatch` on disagreement: a client-chosen cache key would be a cache-poisoning path. |
| `preset` | body | one of `default` | no |  |
| `options` | body | object | no | Optional choices about this image beside its preset. `{}` for none. Two conversions share a cached result only with the same options. Not offered yet: `merge` (a redraw that merges two colors is not offered yet; merge them in the Colors tab, or ask for fewer colors). |
| `idempotencyKey` | body | string | yes | Unique per conversion you mean: a retry with the same key is answered with the conversion it made. |
| `filename` | body | string | no | OPTIONAL: the uploaded file's own name. Kept as a cleaned label (the base name, no extension, at most 80 characters); the download is `<name>.svg`. |
| `unlock` | body | boolean | no | OPTIONAL: unlock the image with this conversion: 1 credit held now, committed when it completes, refunded if it fails. On an image already unlocked it holds nothing (a free re-run); on one a conversion still running is unlocking, 409 `unlock_pending`. No request spends a credit unless it says so. |
| `queue` | body | boolean | no | OPTIONAL: past the active limit, WAIT in line (status `waiting`) instead of a 409 `too_many_active_jobs`. It starts by itself when one of the caller's conversions ends, oldest first, and may be stopped while it waits (POST /conversions/{id}/cancel). Every cap applies at creation: a waiting conversion counts against the day, and a held credit is held while it waits. |

```bash
curl -X POST https://api.vecsnap.com/v1/conversions \
  -H "Authorization: Bearer $VECSNAP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"inputKey":"inputs/2026-10-25/0b6f3c1e-7a2d-4e8b-9c10-5d4e3f2a1b0c/6e1d2c3b-4a5f-4e6d-8c7b-9a0f1e2d3c4b.png","idempotencyKey":"order-1042-logo"}'
```

Answer 200:

```json
{
  "object": "conversion",
  "id": "00000000-0000-4000-8000-000000000000",
  "livemode": false,
  "status": "completed",
  "phase": null,
  "preset": "default",
  "options": {
    "maxColors": 2
  },
  "imageId": "00000000-0000-4000-8000-000000000000",
  "filename": "logo",
  "inputFormat": "png",
  "unlocked": true,
  "allowance": "credit",
  "engineVersion": "1.284.0",
  "result": {
    "anchorPoints": 29,
    "fileSize": 550,
    "formats": [
      {
        "id": "example",
        "profile": "web",
        "background": "kept",
        "currentColor": false,
        "editableText": false,
        "bytes": 0,
        "default": false
      }
    ]
  },
  "failure": null,
  "inputDeletedAt": null,
  "createdAt": "2026-10-24T12:00:00.000Z",
  "updatedAt": "2026-10-24T12:00:00.000Z",
  "cacheHit": false
}
```

Answer 201:

```json
{
  "object": "conversion",
  "id": "00000000-0000-4000-8000-000000000000",
  "livemode": false,
  "status": "completed",
  "phase": null,
  "preset": "default",
  "options": {
    "maxColors": 2
  },
  "imageId": "00000000-0000-4000-8000-000000000000",
  "filename": "logo",
  "inputFormat": "png",
  "unlocked": true,
  "allowance": "credit",
  "engineVersion": "1.284.0",
  "result": {
    "anchorPoints": 29,
    "fileSize": 550,
    "formats": [
      {
        "id": "example",
        "profile": "web",
        "background": "kept",
        "currentColor": false,
        "editableText": false,
        "bytes": 0,
        "default": false
      }
    ]
  },
  "failure": null,
  "inputDeletedAt": null,
  "createdAt": "2026-10-24T12:00:00.000Z",
  "updatedAt": "2026-10-24T12:00:00.000Z",
  "cacheHit": false
}
```

Answer 402:

```json
{
  "error": {
    "code": "insufficient_credits",
    "message": "There are not enough credits for this unlock. None was spent.",
    "requestId": "req_8Gq2Lw0fTz1kVb6s"
  }
}
```

Answer 403:

```json
{
  "error": {
    "code": "account_required",
    "message": "Trying without an account does not include this.",
    "requestId": "req_8Gq2Lw0fTz1kVb6s"
  }
}
```

| Status | Code | What to do |
| --- | --- | --- |
| 402 | `insufficient_credits` | Buy credits, then send the same request again. |
| 403 | `account_required` | Sign in or create an account. The conversions come with you. |
| 404 | `not_found` | Check the id. Another account's objects read as not found too. |
| 409 | `idempotency_key_reused` | Make a new key for a new request. |
| 409 | `too_many_active_jobs` | Wait for one to finish, or send queue: true to wait in line. |
| 409 | `unlock_pending` | Wait for that conversion to finish. |
| 429 | `free_capacity_reached` | Come back after Retry-After (UTC midnight), or unlock with a credit. |
| 429 | `free_limit_reached` | Come back after Retry-After (UTC midnight), or unlock with a credit. |

Every other refusal it may answer: `dimensions_too_large` (400), `file_empty` (400), `file_too_large` (400), `input_hash_mismatch` (400), `invalid_idempotency_key` (400), `invalid_input_key` (400), `invalid_options` (400), `invalid_request` (400), `invalid_size` (400), `malformed_body` (400), `malformed_image` (400), `unknown_preset` (400), `unsupported_type` (400), `invalid_api_key` (401), `unauthenticated` (401), `email_unverified` (403), `plan_required` (403), `account_exists` (409), `email_refused` (422), `rate_limited` (429), `internal_error` (500), `auth_unavailable` (503).

### List conversions

`GET /v1/conversions`

The caller's conversions, newest first, a page at a time: pass a page's `nextCursor` back as `cursor`. With `live=true`, only the LIVE ones (waiting, queued, processing), oldest first: the queue as the server holds it, in one page (`limit` and `cursor` are then refused).

| Field | In | Type | Required | What |
| --- | --- | --- | --- | --- |
| `live` | query | one of `true`, `false` | no | `true`: only the live ones, oldest first. |
| `limit` | query | integer, 1 to 100 | no |  |
| `cursor` | query | string | no | A page's `nextCursor`. |

```bash
curl https://api.vecsnap.com/v1/conversions \
  -H "Authorization: Bearer $VECSNAP_API_KEY"
```

Answer 200:

```json
{
  "object": "list",
  "data": [
    {
      "object": "conversion",
      "id": "00000000-0000-4000-8000-000000000000",
      "livemode": false,
      "status": "completed",
      "phase": null,
      "preset": "default",
      "options": {},
      "imageId": "00000000-0000-4000-8000-000000000001",
      "filename": "logo",
      "inputFormat": null,
      "unlocked": true,
      "allowance": "credit",
      "engineVersion": "sample-27a924dbb20e",
      "result": {
        "anchorPoints": 29,
        "fileSize": 550,
        "formats": [
          {
            "background": "dropped",
            "bytes": 550,
            "currentColor": false,
            "default": true,
            "editableText": false,
            "id": "web",
            "profile": "web"
          },
          {
            "background": "kept",
            "bytes": 600,
            "currentColor": false,
            "default": false,
            "editableText": false,
            "id": "web-background",
            "profile": "web"
          },
          {
            "background": "dropped",
            "bytes": 759,
            "currentColor": false,
            "default": true,
            "editableText": false,
            "id": "design",
            "profile": "design"
          },
          {
            "background": "kept",
            "bytes": 823,
            "currentColor": false,
            "default": false,
            "editableText": false,
            "id": "design-background",
            "profile": "design"
          }
        ]
      },
      "failure": null,
      "inputDeletedAt": null,
      "createdAt": "2026-10-24T12:00:00.000Z",
      "updatedAt": "2026-10-24T12:00:00.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

Every refusal it may answer: `invalid_cursor` (400), `invalid_request` (400), `invalid_api_key` (401), `unauthenticated` (401), `email_unverified` (403), `plan_required` (403), `account_exists` (409), `email_refused` (422), `rate_limited` (429), `internal_error` (500), `auth_unavailable` (503).

### Retrieve a conversion

`GET /v1/conversions/{id}`

A permalink: for the slow tail this is the normal way a conversion is seen, and it answers the same an hour later as while it runs. Another's conversion is 404, the same as none.

| Field | In | Type | Required | What |
| --- | --- | --- | --- | --- |
| `id` | path | id | yes |  |

```bash
curl https://api.vecsnap.com/v1/conversions/00000000-0000-4000-8000-000000000000 \
  -H "Authorization: Bearer $VECSNAP_API_KEY"
```

Answer 200:

```json
{
  "object": "conversion",
  "id": "00000000-0000-4000-8000-000000000000",
  "livemode": false,
  "status": "completed",
  "phase": null,
  "preset": "default",
  "options": {},
  "imageId": "00000000-0000-4000-8000-000000000001",
  "filename": "logo",
  "inputFormat": null,
  "unlocked": true,
  "allowance": "credit",
  "engineVersion": "sample-27a924dbb20e",
  "result": {
    "anchorPoints": 29,
    "fileSize": 550,
    "formats": [
      {
        "background": "dropped",
        "bytes": 550,
        "currentColor": false,
        "default": true,
        "editableText": false,
        "id": "web",
        "profile": "web"
      },
      {
        "background": "kept",
        "bytes": 600,
        "currentColor": false,
        "default": false,
        "editableText": false,
        "id": "web-background",
        "profile": "web"
      },
      {
        "background": "dropped",
        "bytes": 759,
        "currentColor": false,
        "default": true,
        "editableText": false,
        "id": "design",
        "profile": "design"
      },
      {
        "background": "kept",
        "bytes": 823,
        "currentColor": false,
        "default": false,
        "editableText": false,
        "id": "design-background",
        "profile": "design"
      }
    ]
  },
  "failure": null,
  "inputDeletedAt": null,
  "createdAt": "2026-10-24T12:00:00.000Z",
  "updatedAt": "2026-10-24T12:00:00.000Z"
}
```

Answer 404:

```json
{
  "error": {
    "code": "not_found",
    "message": "There is nothing at that id for this caller.",
    "requestId": "req_8Gq2Lw0fTz1kVb6s"
  }
}
```

| Status | Code | What to do |
| --- | --- | --- |
| 404 | `not_found` | Check the id. Another account's objects read as not found too. |

Every other refusal it may answer: `invalid_id` (400), `invalid_api_key` (401), `unauthenticated` (401), `email_unverified` (403), `plan_required` (403), `account_exists` (409), `email_refused` (422), `rate_limited` (429), `internal_error` (500), `auth_unavailable` (503).

### Delete a conversion

`DELETE /v1/conversions/{id}`

Deletes this VERSION of its image: erased at once, it leaves every list and permalink, and its files are deleted within the hour. The image's LAST version is not erased: deleting it deletes the image (Recently deleted, restorable), and `deleted` says which. Its unlock, if paid, stays the image's.

| Field | In | Type | Required | What |
| --- | --- | --- | --- | --- |
| `id` | path | id | yes |  |

```bash
curl -X DELETE https://api.vecsnap.com/v1/conversions/00000000-0000-4000-8000-000000000000 \
  -H "Authorization: Bearer $VECSNAP_API_KEY"
```

Answer 200:

```json
{
  "deleted": "conversion"
}
```

Answer 404:

```json
{
  "error": {
    "code": "not_found",
    "message": "There is nothing at that id for this caller.",
    "requestId": "req_8Gq2Lw0fTz1kVb6s"
  }
}
```

Answer 409:

```json
{
  "error": {
    "code": "conversion_running",
    "message": "The image is still converting.",
    "requestId": "req_8Gq2Lw0fTz1kVb6s"
  }
}
```

| Status | Code | What to do |
| --- | --- | --- |
| 404 | `not_found` | Check the id. Another account's objects read as not found too. |
| 409 | `conversion_running` | Wait for it to finish, then try again. |

Every other refusal it may answer: `invalid_id` (400), `invalid_api_key` (401), `unauthenticated` (401), `email_unverified` (403), `plan_required` (403), `account_exists` (409), `email_refused` (422), `rate_limited` (429), `internal_error` (500), `auth_unavailable` (503).

### Cancel a waiting conversion

`POST /v1/conversions/{id}/cancel`

Only a WAITING conversion (created with `queue: true`, past the active limit) can be stopped: once it has started, a worker holds it. Stopped, it is `canceled`: a held credit comes back, it leaves the library (an image whose only version it was goes with it), and it is not counted against the day.

| Field | In | Type | Required | What |
| --- | --- | --- | --- | --- |
| `id` | path | id | yes |  |

```bash
curl -X POST https://api.vecsnap.com/v1/conversions/00000000-0000-4000-8000-000000000000/cancel \
  -H "Authorization: Bearer $VECSNAP_API_KEY"
```

Answer 200:

```json
{
  "object": "conversion",
  "id": "00000000-0000-4000-8000-000000000000",
  "livemode": false,
  "status": "completed",
  "phase": null,
  "preset": "default",
  "options": {},
  "imageId": "00000000-0000-4000-8000-000000000001",
  "filename": "logo",
  "inputFormat": null,
  "unlocked": true,
  "allowance": "credit",
  "engineVersion": "sample-27a924dbb20e",
  "result": {
    "anchorPoints": 29,
    "fileSize": 550,
    "formats": [
      {
        "background": "dropped",
        "bytes": 550,
        "currentColor": false,
        "default": true,
        "editableText": false,
        "id": "web",
        "profile": "web"
      },
      {
        "background": "kept",
        "bytes": 600,
        "currentColor": false,
        "default": false,
        "editableText": false,
        "id": "web-background",
        "profile": "web"
      },
      {
        "background": "dropped",
        "bytes": 759,
        "currentColor": false,
        "default": true,
        "editableText": false,
        "id": "design",
        "profile": "design"
      },
      {
        "background": "kept",
        "bytes": 823,
        "currentColor": false,
        "default": false,
        "editableText": false,
        "id": "design-background",
        "profile": "design"
      }
    ]
  },
  "failure": null,
  "inputDeletedAt": null,
  "createdAt": "2026-10-24T12:00:00.000Z",
  "updatedAt": "2026-10-24T12:00:00.000Z"
}
```

Answer 404:

```json
{
  "error": {
    "code": "not_found",
    "message": "There is nothing at that id for this caller.",
    "requestId": "req_8Gq2Lw0fTz1kVb6s"
  }
}
```

Answer 409:

```json
{
  "error": {
    "code": "not_waiting",
    "message": "Only a conversion waiting in line can be canceled.",
    "requestId": "req_8Gq2Lw0fTz1kVb6s"
  }
}
```

| Status | Code | What to do |
| --- | --- | --- |
| 404 | `not_found` | Check the id. Another account's objects read as not found too. |
| 409 | `not_waiting` | Let a running conversion finish. |

Every other refusal it may answer: `invalid_id` (400), `invalid_api_key` (401), `unauthenticated` (401), `email_unverified` (403), `plan_required` (403), `account_exists` (409), `email_refused` (422), `rate_limited` (429), `internal_error` (500), `auth_unavailable` (503).

### Unlock a conversion's image

`POST /v1/conversions/{id}/unlock`

1 credit, at most once per image (the same uploaded bytes in this account). After it every result of the image downloads in every format and every re-run of it is free. A repeat, or a conversion of an image already unlocked, spends nothing (`spent: 0`), so a retry is safe. A POST, never folded into the download: a GET that spends a credit is a GET a link previewer spends.

| Field | In | Type | Required | What |
| --- | --- | --- | --- | --- |
| `id` | path | id | yes |  |

```bash
curl -X POST https://api.vecsnap.com/v1/conversions/00000000-0000-4000-8000-000000000000/unlock \
  -H "Authorization: Bearer $VECSNAP_API_KEY"
```

Answer 200:

```json
{
  "unlocked": true,
  "spent": 0
}
```

Answer 402:

```json
{
  "error": {
    "code": "insufficient_credits",
    "message": "There are not enough credits for this unlock. None was spent.",
    "requestId": "req_8Gq2Lw0fTz1kVb6s"
  }
}
```

Answer 404:

```json
{
  "error": {
    "code": "not_found",
    "message": "There is nothing at that id for this caller.",
    "requestId": "req_8Gq2Lw0fTz1kVb6s"
  }
}
```

Answer 409:

```json
{
  "error": {
    "code": "conversion_failed",
    "message": "The conversion ended without a result.",
    "requestId": "req_8Gq2Lw0fTz1kVb6s"
  }
}
```

| Status | Code | What to do |
| --- | --- | --- |
| 402 | `insufficient_credits` | Buy credits, then send the same request again. |
| 404 | `not_found` | Check the id. Another account's objects read as not found too. |
| 409 | `conversion_failed` | Convert the image again. Converting is free. |
| 409 | `not_ready` | Wait for it to finish (a webhook, the events stream, or a read), then ask again. |
| 409 | `result_outdated` | Convert the image again. |
| 409 | `unlock_pending` | Wait for that conversion to finish. |

Every other refusal it may answer: `invalid_id` (400), `invalid_api_key` (401), `unauthenticated` (401), `account_required` (403), `email_unverified` (403), `plan_required` (403), `account_exists` (409), `email_refused` (422), `rate_limited` (429), `internal_error` (500), `auth_unavailable` (503).

### Watch a conversion

`GET /v1/conversions/{id}/events`

Server-Sent Events: a `status` event at once with the current state, then one on each change of `status` or `phase`; its data is a `ConversionEvent`. The stream closes when the conversion ends. A serverless function has a bounded lifetime and a conversion can outlive it, so a client MUST reconnect and re-read the conversion rather than treat this stream as the only way to learn an outcome.

| Field | In | Type | Required | What |
| --- | --- | --- | --- | --- |
| `id` | path | id | yes |  |

```bash
curl https://api.vecsnap.com/v1/conversions/00000000-0000-4000-8000-000000000000/events \
  -H "Authorization: Bearer $VECSNAP_API_KEY"
```

Answer 404:

```json
{
  "error": {
    "code": "not_found",
    "message": "There is nothing at that id for this caller.",
    "requestId": "req_8Gq2Lw0fTz1kVb6s"
  }
}
```

| Status | Code | What to do |
| --- | --- | --- |
| 404 | `not_found` | Check the id. Another account's objects read as not found too. |

Every other refusal it may answer: `invalid_id` (400), `invalid_api_key` (401), `unauthenticated` (401), `email_unverified` (403), `plan_required` (403), `account_exists` (409), `email_refused` (422), `rate_limited` (429), `internal_error` (500), `auth_unavailable` (503).

### Retrieve the verdict

`GET /v1/conversions/{id}/feedback`

The caller's verdict on a result, or null.

| Field | In | Type | Required | What |
| --- | --- | --- | --- | --- |
| `id` | path | id | yes |  |

```bash
curl https://api.vecsnap.com/v1/conversions/00000000-0000-4000-8000-000000000000/feedback \
  -H "Authorization: Bearer $VECSNAP_API_KEY"
```

Answer 200:

```json
{
  "verdict": {
    "vote": "up",
    "reasons": [
      "lost_detail"
    ],
    "note": "example",
    "shareImage": false,
    "updatedAt": "2026-10-24T12:00:00.000Z"
  }
}
```

Answer 404:

```json
{
  "error": {
    "code": "not_found",
    "message": "There is nothing at that id for this caller.",
    "requestId": "req_8Gq2Lw0fTz1kVb6s"
  }
}
```

| Status | Code | What to do |
| --- | --- | --- |
| 404 | `not_found` | Check the id. Another account's objects read as not found too. |

Every other refusal it may answer: `invalid_id` (400), `invalid_api_key` (401), `unauthenticated` (401), `email_unverified` (403), `plan_required` (403), `account_exists` (409), `email_refused` (422), `rate_limited` (429), `internal_error` (500), `auth_unavailable` (503).

### Give a verdict

`PUT /v1/conversions/{id}/feedback`

One per result, given or changed: thumbs up or down; a down's reasons; a note; and `shareImage`. Moves no money: a failed result's credit comes back after a look, by an operator.

| Field | In | Type | Required | What |
| --- | --- | --- | --- | --- |
| `id` | path | id | yes |  |
| `vote` | body | one of `up`, `down` | yes |  |
| `reasons` | body | array of one of `lost_detail`, `wrong_colors`, `rough_edges`, `too_many_shapes`, `text`, `failed` | no | Only with a thumb down. |
| `note` | body | string | no |  |
| `shareImage` | body | boolean | no |  |

```bash
curl -X PUT https://api.vecsnap.com/v1/conversions/00000000-0000-4000-8000-000000000000/feedback \
  -H "Authorization: Bearer $VECSNAP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"vote":"up"}'
```

Answer 200:

```json
{
  "verdict": {
    "vote": "up",
    "reasons": [
      "lost_detail"
    ],
    "note": "example",
    "shareImage": false,
    "updatedAt": "2026-10-24T12:00:00.000Z"
  }
}
```

Answer 404:

```json
{
  "error": {
    "code": "not_found",
    "message": "There is nothing at that id for this caller.",
    "requestId": "req_8Gq2Lw0fTz1kVb6s"
  }
}
```

Answer 409:

```json
{
  "error": {
    "code": "conversion_failed",
    "message": "The conversion ended without a result.",
    "requestId": "req_8Gq2Lw0fTz1kVb6s"
  }
}
```

| Status | Code | What to do |
| --- | --- | --- |
| 404 | `not_found` | Check the id. Another account's objects read as not found too. |
| 409 | `conversion_failed` | Convert the image again. Converting is free. |
| 409 | `not_ready` | Wait for it to finish (a webhook, the events stream, or a read), then ask again. |
| 409 | `result_outdated` | Convert the image again. |

Every other refusal it may answer: `invalid_id` (400), `invalid_request` (400), `invalid_verdict` (400), `malformed_body` (400), `invalid_api_key` (401), `unauthenticated` (401), `email_unverified` (403), `plan_required` (403), `account_exists` (409), `email_refused` (422), `rate_limited` (429), `internal_error` (500), `auth_unavailable` (503).

---
This page: https://vecsnap.com/docs/api/conversions
