# Extras

Bought-in parts shipped with products (designers) or with order lines (integrators).

## Endpoints

-   [GET `/v2/extras`](https://public-api.dapi.digital/docs/reference/extras.md#list_extras)
-   [POST `/v2/extras`](https://public-api.dapi.digital/docs/reference/extras.md#create_extra)
-   [GET `/v2/extras/{id}`](https://public-api.dapi.digital/docs/reference/extras.md#get_extra)
-   [PATCH `/v2/extras/{id}`](https://public-api.dapi.digital/docs/reference/extras.md#update_extra)

## The extra object

A bought-in part that ships with a product, for example a magnet, screw or battery. Extras belong to the account (designer or integrator) that created them and are reviewed by 3DAPI before use.

### Attributes

-   `id` (string): Unique identifier of the extra, prefixed with `xtr_`. Treat it as an opaque string: store it and compare it, never parse it.
-   `object` (string): Names the type of this object. Always `extra`.
-   `name` (string): Display name of the part.
-   `description` (string): What the part is, as written by the seller.
-   `review` (Review): Review by 3DAPI. Anything that will be manufactured is reviewed before it can be sold or produced.
    -   `status` (ReviewStatus):
        
        Where 3DAPI stands on reviewing this object:
        
        -   `pending`: waiting for review by 3DAPI. Nothing to do on your side.
        -   `approved`: reviewed and accepted.
        -   `rejected`: reviewed and refused. `reason` says why; fix it and save again to resubmit.
        
    -   `reason` (string or null): Why it was rejected. Null unless `status` is `rejected`.
    -   `reviewed_at` (timestamp or null): When the latest review decision was made. Null while pending.
-   `price` (object or null): What one unit adds to a variant cost, under `costs.extra_parts` (designers), or to the production price of each printed unit of an order line that carries it (integrators, `items[].extras`). Set by 3DAPI when the extra is approved. While an edited extra is back in review this stays the last approved price. Null until the extra is approved, and null again after a rejection: rejecting clears the price even if the extra was approved before.
    -   `amount` (integer): Amount in the smallest currency unit (cents for usd). 5000 means $50.00. Never a decimal.
    -   `currency` (string): Three-letter ISO 4217 currency code, lowercase. Every amount is `usd` today.
-   `purchase_links` (array of object): Where the production partner can buy the part. At least one.
    -   `url` (string): Where the part can be bought: an http:// or https:// URL. Any other link is refused with `validation_failed`.
    -   `label` (string or null): Short label for the link, for example `Datasheet`.
-   `images` (array of object): Photos of the part. At least one.
    -   `url` (string): Public image URL.
    -   `name` (string): Original file name.
-   `created_at` (timestamp): Point in time, ISO 8601 in UTC.
-   `updated_at` (timestamp): When the extra last changed: created, edited, or approved or rejected by 3DAPI. Extras last changed before this was recorded show the latest of their creation and review decision instead.

**The extra object**

```json
{
  "id": "xtr_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
  "object": "extra",
  "name": "Magnet 10mm",
  "description": "Neodymium magnet, 10mm diameter.",
  "review": {
    "status": "approved",
    "reason": null,
    "reviewed_at": "2026-07-22T10:05:00Z"
  },
  "price": {
    "amount": 50,
    "currency": "usd"
  },
  "purchase_links": [
    {
      "url": "https://example.com/magnet-10mm",
      "label": "Datasheet"
    }
  ],
  "images": [
    {
      "url": "https://cdn.example.com/magnet.png",
      "name": "magnet.png"
    }
  ],
  "created_at": "2026-07-22T09:56:30Z",
  "updated_at": "2026-07-22T10:05:00Z"
}
```

## List extra parts

GET `/v2/extras`

- **Credentials**: An API key with the `extras:read` scope (full access includes every scope). A signed-in dashboard session works too.

- **For**: Designers and Integrators

- **Operation id**: `list_extras`

Lists your extra parts, newest first, with their review status and price. For designers and integrators.

### Query parameters

-   `review_status` (array of ReviewStatus): Only extras in these review states. Same shape as `ReviewStatus` above.
-   `search` (string): Only extras whose name contains this text, ignoring case.
-   `sort` (string): Order of results. A leading `-` means descending. `name` sorts alphabetically in a language-aware collation, which compares letters before spaces and case (`test slicer` comes after `testing warnings`); do not expect byte order.
    
    Possible values: `-created_at`, `created_at`, `name`.
    
    Defaults to "-created\_at".
    
-   `limit` (integer): How many results per page, from 1 to 100.
    
    Range 1 to 100. Defaults to 20.
    
-   `cursor` (string): Where to continue from: the `next_cursor` of the previous page. Omit for the first page.

### Returns 200 OK

A page of extras.

Response attributes (ExtraList):

-   `object` (string): Names the type of this object. Always `list`.
-   `data` (array of Extra): The page of results.
    
    Described in [the extra object](https://public-api.dapi.digital/docs/reference/extras.md#extra_object).
    
-   `has_more` (boolean): True when more results exist after this page. Fetch them by passing `next_cursor` as `cursor`.
-   `next_cursor` (string or null): Opaque cursor for the next page, or null on the last page. Pass it back unchanged as `cursor`.

### Responses

-   `200`: A page of extras.
-   `400`: The request is invalid. `details.errors` lists each problem (`validation_failed`), or an `expand[]` path is unknown (`invalid_expand`).
-   `401`: Credentials are missing (`missing_credentials`), the API key is unknown or inactive (`invalid_api_key`), the session expired (`invalid_session`), or both a key and a session were sent (`ambiguous_credentials`).
-   `403`: The credentials are valid but not allowed to do this: a scope is missing (`missing_scope`), the endpoint is for another kind of account (`actor_not_allowed`) or another kind of credential (`credential_not_allowed`), the account may no longer use the API (`account_blacklisted`), or the account must change something first, in which case `details.fix_url` links to the dashboard page that fixes it.
-   `429`: Too many requests (`rate_limited`): the limit is 500 per minute per API key (or signed-in user). Wait for the number of seconds in the `Retry-After` header, then retry; `details` has `limit`, `window_seconds`, `reset_at` and `retry_after_seconds`.

Errors use [the error object](https://public-api.dapi.digital/docs/guides/errors.md#error-object).

**Request**

```bash
curl "https://public-api.dapi.digital/v2/extras" \
  -H "x-api-key: $DAPI_API_KEY"
```

**Response 200**

```json
{
  "object": "list",
  "data": [
    {
      "id": "xtr_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
      "object": "extra",
      "name": "Magnet 10mm",
      "description": "Neodymium magnet, 10mm diameter.",
      "review": {
        "status": "approved",
        "reason": null,
        "reviewed_at": "2026-07-22T10:05:00Z"
      },
      "price": {
        "amount": 50,
        "currency": "usd"
      },
      "purchase_links": [
        {
          "url": "https://example.com/magnet-10mm",
          "label": "Datasheet"
        }
      ],
      "images": [
        {
          "url": "https://cdn.example.com/magnet.png",
          "name": "magnet.png"
        }
      ],
      "created_at": "2026-07-22T09:56:30Z",
      "updated_at": "2026-07-22T10:05:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

## Create an extra part

POST `/v2/extras`

- **Credentials**: An API key with the `extras:write` scope (full access includes every scope). A signed-in dashboard session works too.

- **For**: Designers and Integrators

- **Operation id**: `create_extra`

Adds an extra part and submits it for review by 3DAPI. It comes back with `review.status: pending` and no price; we set the price when we approve it, and only approved extras can be used: in variants (designers) or on order lines (integrators, `items[].extras` of create\_integrator\_order). Upload the photos first with purpose `extra_image`. For designers and integrators.

### Headers

-   `Idempotency-Key` (string): Any unique string, for example a UUID. Sending the same request again with the same key within 24 hours returns the first response without doing anything twice. Reusing a key with a different body is refused with `idempotency_key_reused`. A refused request (4xx) frees the key so you can correct and resend it; a server error (5xx) is kept and returned again, because the write may already have happened: check the resource before retrying with a new key.

### Request body

-   `name` (string, required): Display name of the part.
-   `description` (string, required): What the part is, for the production partner and our review.
-   `purchase_links` (array of object, required): Where the production partner can buy the part. At least one.
    -   `url` (string, required): Where the part can be bought: an http:// or https:// URL. Any other link is refused with `validation_failed`.
    -   `label` (string or null): Short label for the link, for example `Datasheet`.
-   `images` (array of objects, required): Photos of the part, at least one. Replaces the whole list on update.
    -   Shape **Reference** (default):
        
        -   `upload` (string, required): An upload id from `POST /v2/uploads`, used before it expires. The file is checked when used: one that is not what its name and type say is refused with 422 `invalid_file` (the expected type in `details.expected`) and must be uploaded again.
    -   Shape **Reference** (default):
        
        -   `url` (string, required): The URL of a photo the extra already has, to keep it.

### Returns 201 Created

The extra, pending review. Returns [the extra object](https://public-api.dapi.digital/docs/reference/extras.md#extra_object).

### Responses

-   `201`: The extra, pending review.
-   `400`: The request is invalid (`validation_failed`).
-   `401`: Credentials are missing (`missing_credentials`), the API key is unknown or inactive (`invalid_api_key`), the session expired (`invalid_session`), or both a key and a session were sent (`ambiguous_credentials`).
-   `403`: The credentials are valid but not allowed to do this: a scope is missing (`missing_scope`), the endpoint is for another kind of account (`actor_not_allowed`) or another kind of credential (`credential_not_allowed`), the account may no longer use the API (`account_blacklisted`), or the account must change something first, in which case `details.fix_url` links to the dashboard page that fixes it.
-   `422`: An upload is unknown, expired or of the wrong purpose (`unknown_upload`, `upload_expired`, `upload_purpose_mismatch`, `upload_missing` when the file was never sent, `upload_already_used` when the upload id was already used once), an image `url` is given (a new extra has no photos to keep, `unknown_image`), or the same upload is listed twice (`duplicate_image`).
-   `429`: Too many requests (`rate_limited`): the limit is 500 per minute per API key (or signed-in user). Wait for the number of seconds in the `Retry-After` header, then retry; `details` has `limit`, `window_seconds`, `reset_at` and `retry_after_seconds`.

Errors use [the error object](https://public-api.dapi.digital/docs/guides/errors.md#error-object).

**Request**

```bash
curl -X POST "https://public-api.dapi.digital/v2/extras" \
  -H "x-api-key: $DAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Magnet 10mm",
  "description": "Neodymium magnet, 10mm diameter.",
  "purchase_links": [
    {
      "url": "https://example.com/magnet-10mm",
      "label": "Store"
    }
  ],
  "images": [
    {
      "upload": "upl_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu"
    }
  ]
}'
```

## Get an extra part

GET `/v2/extras/{id}`

- **Credentials**: An API key with the `extras:read` scope (full access includes every scope). A signed-in dashboard session works too.

- **For**: Designers and Integrators

- **Operation id**: `get_extra`

Returns one of your extra parts with its review status and price. For designers and integrators.

### Path parameters

-   `id` (string, required): Unique identifier of the extra, prefixed with `xtr_`. Treat it as an opaque string: store it and compare it, never parse it.

### Returns 200 OK

The extra. Returns [the extra object](https://public-api.dapi.digital/docs/reference/extras.md#extra_object).

### Responses

-   `200`: The extra.
-   `401`: Credentials are missing (`missing_credentials`), the API key is unknown or inactive (`invalid_api_key`), the session expired (`invalid_session`), or both a key and a session were sent (`ambiguous_credentials`).
-   `403`: The credentials are valid but not allowed to do this: a scope is missing (`missing_scope`), the endpoint is for another kind of account (`actor_not_allowed`) or another kind of credential (`credential_not_allowed`), the account may no longer use the API (`account_blacklisted`), or the account must change something first, in which case `details.fix_url` links to the dashboard page that fixes it.
-   `404`: You have no extra with this id (`extra_not_found`).
-   `429`: Too many requests (`rate_limited`): the limit is 500 per minute per API key (or signed-in user). Wait for the number of seconds in the `Retry-After` header, then retry; `details` has `limit`, `window_seconds`, `reset_at` and `retry_after_seconds`.

Errors use [the error object](https://public-api.dapi.digital/docs/guides/errors.md#error-object).

**Request**

```bash
curl "https://public-api.dapi.digital/v2/extras/xtr_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu" \
  -H "x-api-key: $DAPI_API_KEY"
```

## Update an extra part

PATCH `/v2/extras/{id}`

- **Credentials**: An API key with the `extras:write` scope (full access includes every scope). A signed-in dashboard session works too.

- **For**: Designers and Integrators

- **Operation id**: `update_extra`

Changes the fields you send. Sending any field sends the extra back to review by 3DAPI (`review.status: pending`), even if the value is unchanged; a body with no fields changes nothing. Until it is approved again the extra keeps its last approved price. For designers, every product using it goes back to review too (`pending`), which takes it off your store until 3DAPI approves the extra again, exactly as an edit in the dashboard does. For integrators, new orders cannot use it until it is approved again; orders already placed keep it. For designers and integrators.

### Path parameters

-   `id` (string, required): Unique identifier of the extra, prefixed with `xtr_`. Treat it as an opaque string: store it and compare it, never parse it.

### Request body

-   `name` (string): Display name of the part.
-   `description` (string): What the part is, for the production partner and our review.
-   `purchase_links` (array of object): Where the production partner can buy the part. At least one.
    -   `url` (string, required): Where the part can be bought: an http:// or https:// URL. Any other link is refused with `validation_failed`.
    -   `label` (string or null): Short label for the link, for example `Datasheet`.
-   `images` (array of objects): Photos of the part, at least one. Replaces the whole list on update.
    -   Shape **Reference** (default):
        
        Same shape as `UploadReference` above.
        
    -   Shape **Reference** (default):
        
        Same shape as `ExistingImageReference` above.
        

### Returns 200 OK

The extra, pending review. Returns [the extra object](https://public-api.dapi.digital/docs/reference/extras.md#extra_object).

### Responses

-   `200`: The extra, pending review.
-   `400`: The request is invalid (`validation_failed`).
-   `401`: Credentials are missing (`missing_credentials`), the API key is unknown or inactive (`invalid_api_key`), the session expired (`invalid_session`), or both a key and a session were sent (`ambiguous_credentials`).
-   `403`: The credentials are valid but not allowed to do this: a scope is missing (`missing_scope`), the endpoint is for another kind of account (`actor_not_allowed`) or another kind of credential (`credential_not_allowed`), the account may no longer use the API (`account_blacklisted`), or the account must change something first, in which case `details.fix_url` links to the dashboard page that fixes it.
-   `404`: You have no extra with this id (`extra_not_found`).
-   `422`: An upload is unknown, expired or of the wrong purpose (`unknown_upload`, `upload_expired`, `upload_purpose_mismatch`, `upload_missing` when the file was never sent, `upload_already_used` when the upload id was already used once), an image `url` is not one of the photos this extra has (`unknown_image`), or a photo is listed twice (`duplicate_image`).
-   `429`: Too many requests (`rate_limited`): the limit is 500 per minute per API key (or signed-in user). Wait for the number of seconds in the `Retry-After` header, then retry; `details` has `limit`, `window_seconds`, `reset_at` and `retry_after_seconds`.

Errors use [the error object](https://public-api.dapi.digital/docs/guides/errors.md#error-object).

**Request**

```bash
curl -X PATCH "https://public-api.dapi.digital/v2/extras/xtr_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu" \
  -H "x-api-key: $DAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "description": "Neodymium magnet, 10mm diameter, N52 grade.",
  "images": [
    {
      "url": "https://cdn.example.com/magnet.png"
    },
    {
      "upload": "upl_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu"
    }
  ]
}'
```

---

3DAPI API Docs. This page on the web: https://public-api.dapi.digital/docs/reference/extras. Index of every page: https://public-api.dapi.digital/docs/llms.txt.
