# Quality control

Quality checks, partner questions and issue reports on orders.

## Endpoints

-   [POST `/v2/qc-tickets/{id}/review`](https://public-api.dapi.digital/docs/reference/quality-control.md#review_qc_ticket)
-   [POST `/v2/qc-tickets/{id}/messages`](https://public-api.dapi.digital/docs/reference/quality-control.md#send_qc_message)
-   [GET `/v2/qc-tickets`](https://public-api.dapi.digital/docs/reference/quality-control.md#list_qc_tickets)
-   [POST `/v2/qc-tickets`](https://public-api.dapi.digital/docs/reference/quality-control.md#create_qc_ticket)
-   [GET `/v2/qc-tickets/{id}`](https://public-api.dapi.digital/docs/reference/quality-control.md#get_qc_ticket)

## The QC ticket object

A quality check, a question from the production partner, or an issue you reported on an order.

### Attributes

-   `id` (string): Unique identifier of the qc ticket, prefixed with `qct_`. Treat it as an opaque string: store it and compare it, never parse it.
-   `object` (string): Names the type of this object. Always `qc_ticket`.
-   `kind` (QcTicketKind):
    
    What the ticket is, which also says who opened it:
    
    -   `quality_check`: 3DAPI asked the production partner for photos of what it made, before shipping. You accept or refuse each product. Opened by 3DAPI, automatically or by our team.
    -   `question`: the production partner needs an answer from you before it prints. Opened by the production partner.
    -   `issue_report`: you reported a defect after delivery and asked for a replacement. Opened by you; 3DAPI decides the replacement, and that decision closes the report.
    
-   `reason` (string or null): Why a quality check was raised. Null for questions and issue reports.
    
    Possible values: `onboarding`, `spot_check`, `capability`, `suspension`.
    
-   `status` (QcTicketStatus):
    
    Where the ticket is. Quality checks:
    
    -   `awaiting_photos`: the production partner owes photos.
    -   `awaiting_your_review`: photos are in; accept or refuse each product.
    -   `refusal_under_review`: you refused a product and 3DAPI is ruling on it.
    -   `reprinting`: a refusal was upheld and the partner is remaking the product.
    -   `approved`: every product was accepted; the order can ship.
    
    Questions and issue reports:
    
    -   `awaiting_answer`: waiting for a reply: from you on a question, from 3DAPI on an issue report.
    -   `answered`: you answered the question; the production partner has the next word.
    -   `closed`: settled. A question is closed by the production partner who asked it; an issue report closes when 3DAPI decides the replacement.
    
    Any kind:
    
    -   `cancelled`: the ticket no longer applies, for example after the order moved to another partner.
    
-   `needs_your_action` (boolean): True when the next step is yours: photos are waiting for your review, or a question is waiting for your answer.
-   `blocks_shipping` (boolean): True when the order cannot ship until this ticket is approved. Questions and issue reports never block.
-   `order` (object): The order this ticket is about.
    -   `id` (string): Unique identifier of the order, prefixed with `ord_`. Treat it as an opaque string: store it and compare it, never parse it.
    -   `object` (string): Names the type of this object. Always `order`.
    -   `name` (string): Display name of the order.
-   `round` (integer): Current round of photos.
    
    Range at least 1.
    
-   `replacement` (QcReplacement or null): Only on issue reports: the replacement you asked for and what was decided.
    -   `items` (array of object): What you asked to have replaced.
        -   `variant` (VariantReference): Reference to a variant. Pass the matching `expand[]` path to receive the full variant instead.
            -   `id` (string): Unique identifier of the variant, prefixed with `var_`. Treat it as an opaque string: store it and compare it, never parse it.
            -   `object` (string): Names the type of this object. Always `variant`.
            -   `name` (string): Display name of the variant.
        -   `quantity` (integer): Units of that variant you asked to have replaced.
            
            Range at least 1.
            
    -   `decision` (string): `pending` until 3DAPI rules, then `reprint` (a replacement order is made), `handled_by_support` (resolved another way, see the conversation) or `declined`.
    -   `paid_by` (string or null): Who pays for the reprint. Null until decided.
        
        Possible values: `you`, `production_partner`.
        
    -   `order` (object or null): The replacement order, once one was made.
        -   `id` (string): Unique identifier of the order, prefixed with `ord_`. Treat it as an opaque string: store it and compare it, never parse it.
        -   `object` (string): Names the type of this object. Always `order`.
        -   `name` (string): Display name of the order.
-   `created_at` (timestamp): Point in time, ISO 8601 in UTC.
-   `submitted_at` (timestamp or null): When the production partner last submitted photos.
-   `resolved_at` (timestamp or null): When the ticket was approved, closed or cancelled.
-   `items` (array of QcItem): The products on the ticket. A conversation has one container item for its messages.
    -   `id` (string): Unique identifier of the qc item, prefixed with `qci_`. Treat it as an opaque string: store it and compare it, never parse it.
    -   `object` (string): Names the type of this object. Always `qc_item`.
    -   `title` (string or null): What is being checked, usually the variant name.
    -   `product` (ProductReference or null): Reference to a product. Pass the matching `expand[]` path to receive the full product instead.
        -   `id` (string): Unique identifier of the product, prefixed with `prd_`. Treat it as an opaque string: store it and compare it, never parse it.
        -   `object` (string): Names the type of this object. Always `product`.
        -   `name` (string): Display name of the product.
    -   `variant` (object or null): The variant being checked. Null when the item is not linked to a variant.
        -   `id` (string): Unique identifier of the variant, prefixed with `var_`. Treat it as an opaque string: store it and compare it, never parse it.
        -   `object` (string): Names the type of this object. Always `variant`.
        -   `name` (string): Display name of the variant.
    -   `status` (string): Where this product stands: photos owed, photos in and waiting for a verdict, your refusal being ruled on by 3DAPI, or accepted.
        
        Possible values: `awaiting_photos`, `in_review`, `refusal_under_review`, `accepted`.
        
    -   `round` (integer): Which round of photos this is. A new round starts after a refusal is upheld and the product is remade.
        
        Range at least 1.
        
    -   `photos` (array of object): Photos the production partner uploaded, every round.
        -   `id` (string): Identifier of the photo, to point at it in a refusal.
        -   `url` (string): Signed URL to the photo. Valid for 15 minutes; get the ticket again for fresh ones.
        -   `round` (integer): The round of photos this one belongs to.
            
            Range at least 1.
            
        -   `uploaded_at` (timestamp): Point in time, ISO 8601 in UTC.
    -   `history` (array of object): Everything that happened to this product, oldest first.
        -   `action` (string): What happened: photos submitted, product accepted or refused, a refusal upheld or overturned by 3DAPI, or a message asked, answered or closed.
            
            Possible values: `submitted`, `accepted`, `refused`, `refusal_upheld`, `refusal_overturned`, `asked`, `answered`, `closed`.
            
        -   `by` (QcActor): Who did it: you, the production partner or the 3DAPI team.
            
            Possible values: `you`, `production_partner`, `dapi`.
            
        -   `round` (integer): The round of photos this happened in.
            
            Range at least 1.
            
        -   `note` (string or null): The message or reason given, when any.
        -   `at` (timestamp): Point in time, ISO 8601 in UTC.

**The QC ticket object**

```json
{
  "id": "qct_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
  "object": "qc_ticket",
  "kind": "quality_check",
  "reason": "spot_check",
  "status": "awaiting_your_review",
  "needs_your_action": true,
  "blocks_shipping": true,
  "order": {
    "id": "ord_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
    "object": "order",
    "name": "PV-SCN-1701A372"
  },
  "round": 1,
  "replacement": null,
  "created_at": "2026-10-09T15:20:00Z",
  "submitted_at": "2026-10-10T09:01:00Z",
  "resolved_at": null,
  "items": [
    {
      "id": "qci_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
      "object": "qc_item",
      "title": "Clicker Blue",
      "product": {
        "id": "prd_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
        "object": "product",
        "name": "Clicker"
      },
      "variant": {
        "id": "var_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
        "object": "variant",
        "name": "Clicker Blue"
      },
      "status": "in_review",
      "round": 1,
      "photos": [
        {
          "id": "qcp_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
          "url": "https://files.example.com/qc/clicker-front.jpg?signature=abc",
          "round": 1,
          "uploaded_at": "2026-10-10T09:00:00Z"
        }
      ],
      "history": [
        {
          "action": "submitted",
          "by": "production_partner",
          "round": 1,
          "note": null,
          "at": "2026-10-10T09:01:00Z"
        }
      ]
    }
  ]
}
```

## Review a quality check

POST `/v2/qc-tickets/{id}/review`

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

- **For**: Designers

- **Operation id**: `review_qc_ticket`

Accepts or refuses the products on a quality check, all in one request: either every verdict is recorded or none is. Rule on any product whose status is `in_review` (the ticket shows `needs_your_action: true`); this works even while another product on the ticket is already `refusal_under_review`. Accepting every product approves the ticket and lets the order ship. A refusal goes to 3DAPI, which rules on it (`refusal_under_review`); if upheld, the partner reprints the product and sends new photos. Same effect as reviewing in the dashboard. Designers only.

### Path parameters

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

### 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

-   `items` (array of objects, required): One verdict per product you are ruling on.
    -   Shape **verdict: accept**:
        
        -   `item` (string, required): Unique identifier of the qc item, prefixed with `qci_`. Treat it as an opaque string: store it and compare it, never parse it.
        -   `verdict` (string, required): `accept`: the product is fine as photographed.
        -   `note` (string or null): Optional note for the production partner, kept in the history.
    -   Shape **verdict: refuse**:
        
        -   `item` (string, required): Unique identifier of the qc item, prefixed with `qci_`. Treat it as an opaque string: store it and compare it, never parse it.
        -   `verdict` (string, required): `refuse`: the product is not acceptable. 3DAPI rules on the refusal.
        -   `reason` (string, required): What is wrong and what to change. Required: 3DAPI rules on it and the partner reprints from it.
        -   `photos` (array of string): The photos that show the problem: `qcp_` ids of this product's photos from its current round.

### Returns 200 OK

The ticket after your review. Returns [the QC ticket object](https://public-api.dapi.digital/docs/reference/quality-control.md#qc_ticket_object).

### Responses

-   `200`: The ticket after your review.
-   `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.
-   `404`: You have no QC ticket with this id (`qc_ticket_not_found`), or a product is not on it (`qc_item_not_found`, `details.field` names the entry).
-   `409`: The ticket is not a quality check (`not_a_quality_check`), or it is closed or a product is not in review (`not_awaiting_review`).
-   `422`: A product is listed twice (`duplicate_item`), or a refusal points at a photo that is not one of that product's current photos (`photo_not_on_item`).
-   `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/qc-tickets/qct_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu/review" \
  -H "x-api-key: $DAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "items": [
    {
      "item": "qci_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
      "verdict": "accept"
    },
    {
      "item": "qci_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
      "verdict": "refuse",
      "reason": "Layer lines on the front face. Please reprint slower.",
      "photos": [
        "qcp_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu"
      ]
    }
  ]
}'
```

## Reply on a QC conversation

POST `/v2/qc-tickets/{id}/messages`

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

- **For**: Designers

- **Operation id**: `send_qc_message`

Answers a question from the production partner (`kind: question`). Turns alternate: you can post only when the partner spoke last (the ticket is `awaiting_answer`). Photos are uploaded first with purpose `qc_photo`. Issue reports take no messages: 3DAPI answers one by deciding the replacement. Designers only.

### Path parameters

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

### 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

-   `body` (string, required): Your answer to the production partner, up to 5000 characters. Kept in the item history as `answered`.
-   `photos` (array of UploadReference): Photos to attach, uploaded first with purpose `qc_photo`.
    -   `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.

### Returns 200 OK

The ticket with your message. Returns [the QC ticket object](https://public-api.dapi.digital/docs/reference/quality-control.md#qc_ticket_object).

### Responses

-   `200`: The ticket with your message.
-   `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.
-   `404`: You have no QC ticket with this id (`qc_ticket_not_found`).
-   `409`: It is not your turn (`not_your_turn`), the ticket is not a question (`not_a_conversation`), or the question is closed (`ticket_closed`).
-   `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).
-   `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/qc-tickets/qct_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu/messages" \
  -H "x-api-key: $DAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "body": "Yes, print it in matte black. The photo shows where the logo goes.",
  "photos": [
    {
      "upload": "upl_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu"
    }
  ]
}'
```

## List QC tickets

GET `/v2/qc-tickets`

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

- **For**: Designers

- **Operation id**: `list_qc_tickets`

Lists quality checks, questions from production partners and issues you reported, newest first. Filter on `needs_your_action=true` to find what is waiting on you, or on `blocks_shipping=true` to see what holds orders back. Designers only.

### Query parameters

-   `kind` (array of QcTicketKind): Only these kinds. Same shape as `QcTicketKind` above.
-   `status` (array of QcTicketStatus): Only tickets in these states. Same shape as `QcTicketStatus` above.
-   `needs_your_action` (string): Only tickets that are (or are not) waiting on you.
    
    Possible values: `true`, `false`.
    
-   `blocks_shipping` (string): Only tickets that are (or are not) holding an order back.
    
    Possible values: `true`, `false`.
    
-   `order` (string): Only tickets on this order.
-   `created_after` (timestamp): Only tickets opened after this time.
-   `sort` (string): Order of results. A leading `-` means newest first.
    
    Possible values: `-created_at`, `created_at`.
    
    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 tickets.

Response attributes (QcTicketList):

-   `object` (string): Names the type of this object. Always `list`.
-   `data` (array of QcTicketSummary): The page of results.
    -   `id` (string): Unique identifier of the qc ticket, prefixed with `qct_`. Treat it as an opaque string: store it and compare it, never parse it.
    -   `object` (string): Names the type of this object. Always `qc_ticket`.
    -   `kind` (QcTicketKind): Same shape as `QcTicketKind` above.
    -   `reason` (string or null): Why a quality check was raised. Null for questions and issue reports.
        
        Possible values: `onboarding`, `spot_check`, `capability`, `suspension`.
        
    -   `status` (QcTicketStatus): Same shape as `QcTicketStatus` above.
    -   `needs_your_action` (boolean): True when the next step is yours: photos are waiting for your review, or a question is waiting for your answer.
    -   `blocks_shipping` (boolean): True when the order cannot ship until this ticket is approved. Questions and issue reports never block.
    -   `order` (object): The order this ticket is about.
        -   `id` (string): Unique identifier of the order, prefixed with `ord_`. Treat it as an opaque string: store it and compare it, never parse it.
        -   `object` (string): Names the type of this object. Always `order`.
        -   `name` (string): Display name of the order.
    -   `round` (integer): Current round of photos.
        
        Range at least 1.
        
    -   `replacement` (QcReplacement or null): Same shape as `QcReplacement` above.
    -   `created_at` (timestamp): Point in time, ISO 8601 in UTC.
    -   `submitted_at` (timestamp or null): When the production partner last submitted photos.
    -   `resolved_at` (timestamp or null): When the ticket was approved, closed or cancelled.
-   `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 tickets.
-   `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/qc-tickets" \
  -H "x-api-key: $DAPI_API_KEY"
```

**Response 200**

```json
{
  "object": "list",
  "data": [
    {
      "id": "qct_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
      "object": "qc_ticket",
      "kind": "quality_check",
      "reason": "spot_check",
      "status": "awaiting_your_review",
      "needs_your_action": true,
      "blocks_shipping": true,
      "order": {
        "id": "ord_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
        "object": "order",
        "name": "PV-SCN-1701A372"
      },
      "round": 1,
      "replacement": null,
      "created_at": "2026-10-09T15:20:00Z",
      "submitted_at": "2026-10-10T09:01:00Z",
      "resolved_at": null
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

## Report an issue with a delivered order

POST `/v2/qc-tickets`

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

- **For**: Designers

- **Operation id**: `create_qc_ticket`

Opens an issue report on a completed order: what came back wrong, how many, and photos (uploaded first with purpose `qc_photo`). 3DAPI reviews it and decides on a replacement and who pays for it (`replacement`); that decision closes the report. The production partner is not part of the report. It never holds an order. Designers only.

### 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

-   `order` (string, required): The delivered order the issue is about. It must be `completed`.
-   `items` (array of object, required): What came back wrong and needs replacing.
    -   `order_item` (string, required): Unique identifier of the order item, prefixed with `itm_`. Treat it as an opaque string: store it and compare it, never parse it.
    -   `quantity` (integer, required): Units to replace, at most the quantity ordered on that item (more is refused with `quantity_exceeds_ordered`).
        
        Range 1 to 1000.
        
-   `message` (string, required): What happened, for 3DAPI to rule on.
-   `photos` (array of UploadReference): Photos to attach, uploaded first with purpose `qc_photo`. Same shape as `UploadReference` above.

### Returns 201 Created

The new issue report. Returns [the QC ticket object](https://public-api.dapi.digital/docs/reference/quality-control.md#qc_ticket_object).

### Responses

-   `201`: The new issue report.
-   `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.
-   `404`: You have no such order (`order_not_found`), or an item is not on it (`order_item_not_found`, `details.field` names the entry).
-   `409`: Issues can only be reported on completed orders (`order_not_eligible`).
-   `422`: An item is listed twice (`duplicate_item`), asks for more units than were ordered (`quantity_exceeds_ordered`, `details.ordered`), or 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).
-   `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/qc-tickets" \
  -H "x-api-key: $DAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "order": "ord_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
  "items": [
    {
      "order_item": "itm_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
      "quantity": 1
    }
  ],
  "message": "The lid arrived cracked along the hinge.",
  "photos": [
    {
      "upload": "upl_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu"
    }
  ]
}'
```

## Get a QC ticket

GET `/v2/qc-tickets/{id}`

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

- **For**: Designers

- **Operation id**: `get_qc_ticket`

Returns one ticket with every product on it, its photos and the history of decisions and messages. Designers only.

### Path parameters

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

### Returns 200 OK

The ticket. Returns [the QC ticket object](https://public-api.dapi.digital/docs/reference/quality-control.md#qc_ticket_object).

### Responses

-   `200`: The ticket.
-   `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 QC ticket with this id (`qc_ticket_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/qc-tickets/qct_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu" \
  -H "x-api-key: $DAPI_API_KEY"
```

---

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