{"record":{"id":"67599e391793985d","repo":"medusajs/medusa","slug":"sales-channels-can-only-be-associated-with-publish","errorCode":null,"errorMessage":"Sales channels can only be associated with publishable API keys","messagePattern":"Sales channels can only be associated with publishable API keys","errorType":"http","errorClass":"MedusaError","httpStatus":400,"severity":"error","filePath":"packages/medusa/src/api/admin/api-keys/[id]/sales-channels/route.ts","lineNumber":21,"sourceCode":"import { ApiKeyType, MedusaError } from \"@medusajs/framework/utils\"\nimport {\n  AuthenticatedMedusaRequest,\n  MedusaResponse,\n} from \"@medusajs/framework/http\"\nimport { refetchApiKey } from \"../../helpers\"\n\nexport const POST = async (\n  req: AuthenticatedMedusaRequest<\n    HttpTypes.AdminBatchLink,\n    HttpTypes.SelectParams\n  >,\n  res: MedusaResponse<HttpTypes.AdminApiKeyResponse>\n) => {\n  const { add, remove } = req.validatedBody\n  const apiKey = await refetchApiKey(req.params.id, req.scope, [\"id\", \"type\"])\n\n  if (apiKey.type !== ApiKeyType.PUBLISHABLE) {\n    throw new MedusaError(\n      MedusaError.Types.INVALID_DATA,\n      \"Sales channels can only be associated with publishable API keys\"\n    )\n  }\n\n  await linkSalesChannelsToApiKeyWorkflow(req.scope).run({\n    input: {\n      id: req.params.id,\n      add,\n      remove,\n    },\n  })\n\n  const updatedApiKey = await refetchApiKey(\n    req.params.id,\n    req.scope,\n    req.queryConfig.fields\n  )","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/medusajs/medusa/blob/5e06e544a296b9033f20f71f11c559f81a0e5739/packages/medusa/src/api/admin/api-keys/[id]/sales-channels/route.ts#L3-L39","documentation":"The POST /admin/api-keys/:id/sales-channels route only allows linking sales channels to publishable API keys. The route refetches the key's type and rejects with INVALID_DATA (HTTP 400) when the key is a secret key.","triggerScenarios":"POST /admin/api-keys/:id/sales-channels with { add, remove } where the target key has type 'secret'; attempting to reuse secret-key management endpoints as if they were publishable-key endpoints.","commonSituations":"UI dropdown listing all API keys without filtering by type; automation scripts that assume any key can be scoped to sales channels; creating the key with the wrong type initially.","solutions":["Create a publishable API key (POST /admin/api-keys with type=publishable) and link channels to that","Filter key lists by type=publishable in your admin UI before showing the link action","Check the key's type via GET /admin/api-keys/:id before calling"],"exampleFix":"// before\nconst key = await sdk.apiKey.create({ title: \"store\", type: \"secret\" })\nawait sdk.apiKey.linkSalesChannels(key.id, { add: [scId] }) // 400\n\n// after\nconst key = await sdk.apiKey.create({ title: \"store\", type: \"publishable\" })\nawait sdk.apiKey.linkSalesChannels(key.id, { add: [scId] })","handlingStrategy":"validation","validationCode":"const { api_key } = await sdk.apiKey.retrieve(keyId, { fields: [\"id\",\"type\"] })\nif (api_key.type !== \"publishable\") {\n  throw new Error(\"Link sales channels only to publishable keys\")\n}","typeGuard":"const isPublishableKey = (k: { type: string }) => k.type === \"publishable\"","tryCatchPattern":"try { await link(keyId, payload) } catch (e) { if (e.type === \"invalid_data\" && /publishable/.test(e.message)) { /* recreate key as publishable */ } throw e }","preventionTips":["Filter API key lists by type before offering link actions","Create publishable keys for storefront scoping from the start"],"tags":["admin-api","api-keys","sales-channels","invalid-data"],"backgroundTag":"api-invalid-resource-type","analyzedSha":"5e06e544a296b9033f20f71f11c559f81a0e5739","analyzedAt":"2026-08-27T07:24:39.599Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}