# Onboarding chat turn

Source: https://docs.musicnerd.net/api-reference/onboarding/chat

Onboarding chat turn

## POST /api/onboarding/{artistId}/chat

Full OpenAPI specification: https://docs.musicnerd.net/spec/onboarding.json

## Authentication

This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification.

[Authentication guide](https://docs.musicnerd.net/authentication)

## Operation and referenced schemas

```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "Music Nerd API: onboarding",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://musicnerd-api.vercel.app",
      "description": "Production"
    },
    {
      "url": "https://musicnerd-api-staging.vercel.app",
      "description": "Staging (staging database)"
    }
  ],
  "paths": {
    "/api/onboarding/{artistId}/chat": {
      "post": {
        "summary": "Onboarding chat turn",
        "operationId": "postOnboardingChatTurn",
        "security": [
          {
            "privyBearer": []
          }
        ],
        "description": "One turn of an artist's onboarding, streamed as server-sent events. The server owns the step order (profiles → vault → interview → publish): a turn for a step that is not current gets an `error` event and the real step. Confirmed steps are resumed rather than repeated. Claim approval grants editing rights; it never approves sources or publishes an About. A turn stops at 55 seconds and says so, and the server finishes the work even if the client disconnects.\n\nEach event is a `data:` line holding one JSON object with a `kind`: `chat`, `progress`, `step`, `candidate`, `linked`, `unreachable`, `source`, `sources`, `text-delta`, `choices`, `draft`, `complete` or `error`. A turn ends with `step`, `draft`, `complete` or `error`.\n\nThe initial interview prepares up to three questions once from already saved material, with one 15-second total preparation budget. It does not wait for a social scrape. Drafting and source verification share that deadline; timed-out model work is cancelled and only verified questions may be used. Remaining slots use distinct optional general questions. The private question plan is reused after answers, skips, reloads and server restarts, scoped to the current claim. Choosing a fallback plan does not retry generation on each skip. Prepared questions are not artist testimony; only explicit answers/skips create answer records. Saved history is checked before offering the next question; a history read failure returns a recoverable error rather than restarting or completing the interview. Each answered/skipped key counts once toward the three-question cap. Existing answer revisions and explicit publication are unchanged.",
        "parameters": [
          {
            "name": "artistId",
            "in": "path",
            "required": true,
            "description": "The artist's ID (a UUID). Staging and production IDs differ.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "aab92f80-f9e1-4299-aa33-7dd85c8de5d3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "title": "open",
                    "required": [
                      "type"
                    ],
                    "properties": {
                      "type": {
                        "const": "open"
                      }
                    },
                    "description": "Start or resume. A fresh claim uses completed directory discovery when its versioned checkpoint is at most 24 hours old, belongs to the original directory-add event, and still matches the artist identity and source content. Approving or rejecting sources does not invalidate that discovery. Reuse shows saved profiles and pending sources for explicit review without repeating profile or web discovery. Saved Deezer artist IDs and YouTube channel IDs render as their canonical public profile URLs, and a stored channel counts as an existing YouTube profile; missing, incomplete, stale or changed discovery does not suppress a new search. Discovery completion has its own immutable timestamp; a parent job still pending, running or failed while handing off later work can reuse the completed discovery. Downstream queue retries do not restart discovery or reset an existing child job. Social collection and caption processing have separate completion state. The artist can still request more profiles or refresh research explicitly. Opening never approves sources or publishes an About."
                  },
                  {
                    "type": "object",
                    "title": "confirm_profiles",
                    "required": [
                      "type",
                      "addedLinks",
                      "removedSiteNames"
                    ],
                    "properties": {
                      "type": {
                        "const": "confirm_profiles"
                      },
                      "addedLinks": {
                        "type": "array",
                        "maxItems": 100,
                        "items": {
                          "type": "object",
                          "required": [
                            "url"
                          ],
                          "properties": {
                            "url": {
                              "type": "string"
                            }
                          }
                        }
                      },
                      "removedSiteNames": {
                        "type": "array",
                        "maxItems": 100,
                        "items": {
                          "type": "string"
                        }
                      }
                    },
                    "description": "Confirm the profiles step, adding and removing links."
                  },
                  {
                    "type": "object",
                    "title": "find_more_profiles",
                    "required": [
                      "type"
                    ],
                    "properties": {
                      "type": {
                        "const": "find_more_profiles"
                      },
                      "addedLinks": {
                        "type": "array",
                        "maxItems": 100,
                        "items": {
                          "type": "object",
                          "required": [
                            "url"
                          ],
                          "properties": {
                            "url": {
                              "type": "string"
                            }
                          }
                        }
                      },
                      "removedSiteNames": {
                        "type": "array",
                        "maxItems": 100,
                        "items": {
                          "type": "string"
                        }
                      }
                    },
                    "description": "Apply link changes and search again, without confirming the step."
                  },
                  {
                    "type": "object",
                    "title": "vault_review",
                    "required": [
                      "type",
                      "decisions",
                      "addedUrls"
                    ],
                    "properties": {
                      "type": {
                        "const": "vault_review"
                      },
                      "decisions": {
                        "type": "array",
                        "maxItems": 100,
                        "items": {
                          "type": "object",
                          "required": [
                            "sourceId",
                            "status"
                          ],
                          "properties": {
                            "sourceId": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "approved",
                                "rejected"
                              ]
                            }
                          }
                        }
                      },
                      "addedUrls": {
                        "type": "array",
                        "maxItems": 100,
                        "items": {
                          "type": "string"
                        }
                      }
                    },
                    "description": "Explicitly approve or reject the displayed sources and add URLs. All pending sources must be reviewed before moving on. Previously rejected sources stay rejected; claim approval and opening the chat do not approve them. Generated text can reuse a private prepared draft only when every cited original is unchanged and approved and no newer artist material would be omitted."
                  },
                  {
                    "type": "object",
                    "title": "interview_answer",
                    "required": [
                      "type",
                      "questionKey",
                      "answer"
                    ],
                    "properties": {
                      "type": {
                        "const": "interview_answer"
                      },
                      "questionKey": {
                        "type": "string"
                      },
                      "answer": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "`null` skips the question."
                      },
                      "question": {
                        "type": "string",
                        "description": "The question text, for grounded (`social_…`) questions."
                      }
                    },
                    "description": "Answer or skip one interview question."
                  },
                  {
                    "type": "object",
                    "title": "about_choice",
                    "required": [
                      "type",
                      "mode",
                      "doc"
                    ],
                    "properties": {
                      "type": {
                        "const": "about_choice"
                      },
                      "mode": {
                        "type": "string",
                        "enum": [
                          "generate",
                          "self"
                        ]
                      },
                      "doc": {
                        "type": "string"
                      },
                      "sources": {
                        "type": "array",
                        "items": {
                          "type": "object"
                        }
                      }
                    },
                    "description": "Generate the About from the doc, or write it yourself."
                  },
                  {
                    "type": "object",
                    "title": "publish",
                    "required": [
                      "type",
                      "doc",
                      "about",
                      "expectedBio"
                    ],
                    "properties": {
                      "type": {
                        "const": "publish"
                      },
                      "doc": {
                        "type": "string"
                      },
                      "about": {
                        "type": "string"
                      },
                      "sources": {
                        "type": "array",
                        "items": {
                          "type": "object"
                        }
                      },
                      "expectedBio": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "The bio the draft started from; a different current bio is refused."
                      }
                    },
                    "description": "Publish the About and save the doc; confirms the publish step."
                  }
                ]
              },
              "example": {
                "type": "open"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A stream of turn events.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                },
                "example": "data: {\"kind\":\"chat\",\"text\":\"Your profile is yours. Building your page now, which takes a moment.\"}\n\ndata: {\"kind\":\"progress\",\"label\":\"Finding your profiles\",\"done\":false,\"group\":\"platform-search\"}\n\ndata: {\"kind\":\"complete\"}\n\n"
              }
            }
          },
          "400": {
            "description": "The artist ID is not a UUID, the body is not JSON or not a known turn, or an array has more than 100 items.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "error": "Invalid turn"
                }
              }
            }
          },
          "401": {
            "description": "No token, an invalid or expired token, or no Music Nerd user for it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "error": "Not signed in"
                }
              }
            }
          },
          "403": {
            "description": "The user is neither the artist's claimant nor an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "error": "Not your artist"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "status",
          "error"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "error"
            ]
          },
          "error": {
            "type": "string"
          }
        }
      }
    },
    "securitySchemes": {
      "privyBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Access token",
        "description": "The signed-in user's access token. Copy it at https://musicnerd.net/access after signing in. The user must be the artist's approved claimant or a Music Nerd admin."
      }
    }
  }
}
```
