Intake

Description

Integrate Intake over its stateless responses API so unstructured customer input (an email, support ticket, PDF, spreadsheet, CSV, or free text, including an inbound quote or purchase order) becomes one Cart, Quote Request, or Shopping List. Use when connecting that input to Intake (also called the Intake Agent), including outputType, file upload, and writing the created id back to the channel. Not the Merchant Center Intake UI, and not ongoing ERP, CRM, or transactional-email sync (see commercetools-integrations).

Installation

Recommended: install the full commercetools plugin. It includes this Skill, every other commercetools Skill, our pre-tuned Subagents, and the commercetools Knowledge MCP — which gives AI live access to the commercetools docs, GraphQL/OpenAPI schemas, and query validation. You only install once; every Skill on this site becomes available in every session.
Install the plugin

In any Claude Code session, run the following commands one at a time:

  1. Add the commercetools AI marketplace:

    /plugin marketplace add commercetools/commercetools-ai-plugins
    
  2. Install the commercetools AI plugin (Skills + Subagents + MCP):

    /plugin install commercetools@commercetools
    
Reload plugins

If you've updated the plugin or installed it in another window and need the current session to pick up the latest version:

/reload-plugins
Claude Desktop
Customize -> Personal plugins -> Create plugin -> Add marketplace -> Add commercetools/commercetools-ai-plugins. Then, click on the plugin and click Install.

Instructions Included

SKILL.md

Intake

Call Intake so a channel adapter can turn one unstructured request into one commercetools entity. The HTTP surface is a single stateless endpoint. Each call starts a fresh run and returns either a created entity or an error. It does not resume a previous run, so a follow-up email or ticket comment is not another turn of the same call.

The adapter owns three decisions Intake will not make: which entity to create, which Customer the message is for, and whether this inbound event is a new request or a reply to an entity already created.

Out of scope

  • Building or automating the Merchant Center Intake UI. Use that UI once, before writing the adapter, to confirm the same prompt creates the entity. The UI can ask a clarifying question. This endpoint cannot.
  • Connect connectors, transactional email, and CRM sync. Those belong to commercetools-integrations.
  • Quote negotiation after a Quote Request exists (Staged Quote, Quote, Order). That is the Quotes API.

Workflow

  1. Docs search (required, run first) — The first time you use this skill in a session you must run this before answering. It gathers the latest verified documentation as your primary grounding, filtered to the products this skill covers. Use this script for documentation search while working with this skill; the Knowledge MCP covers everything else. Always confirm details against retrieved documentation rather than the skill text alone:
    node "<this skill's directory>/scripts/docs-search.mjs" \
      --query "Intake responses API Cart Quote Request Shopping List file upload" \
      --app-name "<host app: claude-code, claude-chat, cursor, codex, copilot — or the host's own name>" \
      --model "<current-model>" \
      --commercetools-project-key "<if known>" \
      --commercetools-region "<if known>" \
      --limit 10
    
    Pass the commercetools project key and region (as in api.{region}.commercetools.com) only if already in your context; otherwise omit both. Never search files or ask the user for them.
  2. Fetch the REST schema before writing a request or parsing a response. Run:
    node scripts/openApi-schemata.mjs \
      --resource-name "api-AgentResponsesSuccess" \
      --app-name "<host app: claude-code, claude-chat, cursor, codex, copilot — or the host's own name>" \
      --model "<current-model>"
    
    The output is the OpenAPI specification for that resource. If the name is not recognized, the script prints valid names. Re-run with api-Cart-write, api-QuoteRequest, or api-ShoppingList when the follow-up is an update to the entity Intake created, not another Intake call.
  3. Confirm the Project can accept the call. Intake is enabled for the Project (otherwise FeatureDisabled). The API client has the manage_intake_agent:{projectKey} scope. Product Search is enabled (otherwise ProductSearchNotEnabled). The buyer already exists as a Customer. Intake looks a Customer up by email and does not create one.
  4. Run the same prompt in Merchant Center before writing the adapter. Paste the prompt and any file into Merchant Center Intake. A failure there is the Project, catalog, or Customer. A success there and a failure from the API is the request the integration sent. A success that needed a clarifying question does not count: the prompt that produced the entity is the prompt the API body must contain.
  5. Choose outputType. Read choose-output.md. The field is required. Intake never infers it from the message.
  6. Map the channel into one request. Read call-responses.md, then the channel file:
    • Mailbox: email.md
    • Zendesk: zendesk.md
    • Any other drop (a form, a drive folder, another helpdesk): the same mapping in call-responses.md. A new channel is a new normalizer, not a new Intake contract.
  7. Call once per new request, and remember that you did. The endpoint has no idempotency key. Conditionally insert the channel's own id (email Message-ID, Zendesk ticket id) with the normalized request as queued work, acknowledge the delivery, then mark the row as a call with an unknown outcome immediately before the POST. On success, store that id against threadId, entity.id, and, for a Quote Request, cartId. Recovery may send a queued row that was never marked. It must not resend a marked row. A second POST creates a second entity.
  8. Handle the outcome and write it back using handle-outcome.md. A 201 can still carry warnings. A 400 names what was missing. Do not answer a 400 by calling again with the same body.
  9. Route a later message on the same thread by the stored outcome. An entity already stored is updated with the normal Cart, Quote Request, or Shopping List APIs. A confirmed no-write failure (a 400 that created nothing) allows one corrected Intake attempt when the reply supplies the missing input. A pending queued row, a call with an unknown outcome, or a partial creation (QuoteRequestCreationFailed with cartId) is resolved from that stored state. Do not POST again to finish it.

Checklist

  • The same prompt created the entity in Merchant Center before the adapter was written. A clarifying question in that UI does not count
  • outputType was chosen by the integration, not left for Intake to guess
  • The prompt states the buyer's email, and for a Cart or Quote Request a shipping country, in plain text
  • A Shopping List name is stated in the prompt or a file. Intake does not invent one
  • businessUnit and store are passed as resource identifiers when the channel knows them
  • Uploads use the files part, at most 3 files, only allowed content types
  • The channel event id and the normalized request are stored as queued work before the webhook is acknowledged. A call is marked as an unknown outcome immediately before the POST, then stored against entity.id and threadId. Recovery sends unstarted work and does not resend a call that may have completed
  • Warnings and the specific 400 code are written back to the channel
  • A follow-up on the same email thread or ticket follows the stored outcome: update an existing entity, allow one corrected Intake attempt after a confirmed no-write failure when the reply supplies the missing input, and resolve a pending, unknown, or partial-creation outcome without a blind retry

References

call-responses.md

Call the responses endpoint

POST /{projectKey}/agents/intake/v1/responses on https://api.{region}.commercetools.com. Reference: Intake. Schema: api-AgentResponsesSuccess via openApi-schemata.mjs.
Authenticate like any other HTTP API call for the Project. The client needs manage_intake_agent:{projectKey}. A 401 is invalid_token. A 403 is insufficient_scope. Both use AuthErrorResponse, not the Intake error envelope.
The call does extraction and catalog matching before it returns, so it is slow. The responses endpoint has no idempotency key. Persist the normalized request as queued work, acknowledge the webhook, then call. The sequence is in Idempotency belongs to the adapter.

What the body can express

FieldRole
outputTypeCart, QuoteRequest, or ShoppingList. Required. Never inferred.
localeBCP-47 language tag of the message, such as en-US or de-DE. Required. This is the language of the Shopping List name and of product-name matching. It does not set the shipping country.
promptThe natural-language input. Required on application/json. Optional on multipart when at least one file is attached.
businessUnitOptional resource identifier. Pass key when the channel knows the unit.
storeOptional resource identifier. Same rule.
Customer email, shipping country, line items, and the Shopping List name are not request fields. Intake reads them from prompt and from files. If the channel already knows one of them, write it at the top of the prompt in plain sentences. Extraction then does not have to recover it from a signature or a ship-to block:
{
  "outputType": "Cart",
  "locale": "de-DE",
  "businessUnit": { "typeId": "business-unit", "key": "acme-west" },
  "store": { "typeId": "store", "key": "west-coast" },
  "prompt": "Customer email: jane.doe@example.com. Shipping country: DE.\n\nBitte 12x ABC-123 und 4x XYZ-789 bis Freitag."
}
locale stays the language. Shipping country: DE is what satisfies the country requirement. A German mailbox with locale: de-DE and no country in the text still fails with MissingCountry for a Cart or Quote Request.

Why the structured fields exist

Free-text inference is the source of BusinessUnitUnresolved, BusinessUnitAmbiguous, StoreUnresolved, and StoreAmbiguous. Pass businessUnit and store when a mailbox route, a ticket field, or an organization mapping already decided them.
The reference still has to be one this Customer can use. A pinned Business Unit the Customer is not associated with fails with BusinessUnitUnresolved. A pinned Store the Customer cannot order from through that unit fails with StoreUnresolved. Map the channel's organization to the commercetools key. Do not send the external organization's id and expect Intake to translate it.
Passing one side is enough when the other is unambiguous: one Business Unit implies its Store, and one Store can narrow the Business Unit. When the Customer belongs to several units and the message names none, pass businessUnit rather than hoping the text is clear enough.
BusinessUnitLimitExceeded means the Customer's units could not be listed. Pin businessUnit (and store if it is still ambiguous) and call again only if the first call did not create an entity.
A Store with more than one distribution channel fails with StoreDistributionChannelsUnsupported. Choose a Store with zero or one.

Files

Send multipart/form-data when there is a document. One payload part, JSON, content type application/json. Every file under the part name files. Any other part name is InvalidField.
Limits and allowed types are on Multipart form data: 3 files, 20 MB each, PDF, plain text, CSV, DOCX, XLSX, EML (message/rfc822), Markdown, HTML. Images are not allowed. A file over the size limit is ContentTooLarge (413).

Prefer the purchase order over logos and signature images. Three slots is the whole budget.

An uploaded .eml is one of those three files. Intake reads its From as the customer email and also reads attachments nested inside it. Inline related images, including most signatures, are skipped and do not fail the file. A non-related attachment of a disallowed type fails the whole file as FileNotProcessed. In that case, put the body in prompt and attach only the document files.
Use .eml when the message is the buyer's own mail and its attachments are all allowed types. The From address is then the Customer email, so the prompt preamble is only needed for country, list name, or a Business Unit the headers do not carry.

One call, no resume

There is no conversation id to send back. threadId on the response identifies that run for support. It is not a handle for a second call.
If required information is missing, fix the prompt or the reference and POST again only when you know the first call created nothing. See handle-outcome.md.

Idempotency belongs to the adapter

The endpoint will happily create two Carts for one email. Two overlapping deliveries that both see an empty key both POST, so the request has to be stored before the HTTP acknowledgement returns. An in-progress marker alone cannot recover that request: if the process exits after acknowledging and before the POST, the row stays in progress and every redelivery is discarded.

  1. Conditionally insert the channel event id together with the normalized request, as queued work that has not been sent. If that insert does not win — the id is already queued, a call is already in an unknown state, or the id already has an entity.id — acknowledge the duplicate so the channel stops redelivering, and stop. Do not call from this delivery. The stored row remains the copy recovery runs.
  2. Acknowledge the delivery. The queued request is now the only copy that can still run.
  3. Mark that row as a call with an unknown outcome immediately before the POST, then call Intake once. Recovery may send a queued row that was never marked this way. It must not send a row already marked: that POST may have completed, and a second call creates a second entity.
  4. On 201, store entity.id, entityType, threadId, and cartId when present, against the same id.
  5. On a 400 that names missing input, store the failure against the same id so a redelivery does not repeat it. Correct the input only when you have new information, then allow one new attempt.
ExtractionFailed (500) happens before a commercetools write. A single retry of that code is safe if the row still has no entity.id. QuoteRequestCreationFailed is not safe to retry: the Cart already exists. See handle-outcome.md.
choose-output.md

Choose the output

outputType is the integration's decision. Intake does not pick from the wording of the email. If the business has not decided, ask before generating the client. The required inputs per type are in Required information.
The request is…outputTypeThe prompt must also contain
Something to price and fulfill at the Customer's entitled pricesCartBuyer email, shipping country, at least one product reference
A request for terms (volume, a split delivery, "please quote")QuoteRequestThe same as a Cart. The Customer must already exist; the created Cart has to carry that verified Customer
A named list to buy later, or a list that may include unpublished productsShoppingListBuyer email and a name for the list. Country is not used
All three require at least one product reference: a name, a product key, a variant SKU, or a variant key. With no line items at all the call fails with NoLineItemsExtracted. Some unmatched products are a warning, not a failure. See handle-outcome.md.

Cart

Use this when the merchant will review the basket and then freeze it, order it, or turn it into a Quote Request themselves. The response entity is a Cart in REST form.
Country is read from the input (a stated country or a ship-to address), not from locale and not from the Customer record. A marketplace mailbox that only receives German orders should still put Shipping country: DE in the prompt.

Quote Request

Use this when the inbound message is the buyer's ask and a seller will negotiate before anything is binding. Intake creates the Cart and the Quote Request. The 201 body includes entity (the Quote Request) and cartId (the Cart it was created from). Keep both.
If the Cart is created but the Quote Request is not, the error is QuoteRequestCreationFailed and it carries cartId of that Cart. Do not call Intake again to "finish" it. That creates another Cart. Create the Quote Request from cartId with the Quote Requests API. Deleting the Cart is the other option, and it removes customer data. Show the exact proposed deletion request and obtain the user's explicit confirmation before executing it.
A Quote Request needs a verified Customer on the source Cart. An email that matches no Customer fails the call before a Cart exists. QuoteRequestCreationFailed is the later case, where the Cart exists and the Quote Request does not. Create or correct the Customer first, then retry only if no cartId was stored.

After the Quote Request exists, seller follow-up is the normal quote flow, not another Intake call.

Shopping List

Use this when the outcome is a list the Customer will turn into a Cart later, or when the products may be unpublished. A Cart or Quote Request cannot hold unpublished products. A Shopping List can: an exact key, SKU, variant key, or product name in locale (or a project or store language) is looked up against staged data, and the line item is stored with published: false. Partial names do not match. Details: Unpublished Products in a Shopping List.
The name has to be in the prompt or a file, for example Create a shopping list called "Q3 restock". Intake does not invent one. A missing name is MissingShoppingListName and nothing is created. When the channel has a natural name (the email subject, the ticket subject), state that name in the prompt rather than asking the model to title it.
The name is stored as a LocalizedString under locale. A description in the input is stored the same way.

Country is not required and is not used for pricing.

email.md

Email

Read call-responses.md first. This file is only the mailbox mapping.
The idempotency key is the RFC 5322 Message-ID. Use the same sequence as call-responses.md: conditionally insert that id with the normalized request as queued work, acknowledge the provider delivery, then mark the row as a call with an unknown outcome immediately before the POST. A redelivery of the same Message-ID loses the insert and must not call Intake again. Recovery sends a queued row that was never marked, and does not resend a marked row.

Which address is the Customer

Intake looks up one existing Customer by email. The address it needs is the buyer's, not the mailbox that received the mail (orders@your-domain.com) and not the address of whoever forwarded it internally.
What you receivedCustomer email
The buyer's own messageThe From address, if that address is a Customer in the Project
A forwardThe original From inside the body, not the forwarder's
A helpdesk or automation notificationThe buyer named in the body, not the notification sender
Put that address in the prompt as Customer email: jane.doe@example.com. When you upload the buyer's own .eml, Intake already treats From as the customer email. A forwarded or notification .eml will treat the wrong From as the customer, so do not upload those. Use the parsed body plus an explicit preamble instead.
If the address matches no Customer, the call fails. Create the Customer with the Customers API, then allow one new attempt for that Message-ID. Intake will not create the Customer for you.

What to send

State, in the prompt, anything the route already knows:

  • Customer email: …
  • Shipping country: DE for a Cart or Quote Request. A dedicated country mailbox is not a country unless you write it out. locale does not carry it.
  • For a Shopping List, Create a shopping list called "{subject}" using the subject when the body never names the list.

Then the body.

Attachments:

  • Keep PDF, CSV, XLSX, DOCX, HTML, and plain text. Drop images. The cap is 3 files, 20 MB each.
  • Upload the buyer's raw .eml (message/rfc822) when its non-related attachments are allowed types. Inline related images, including most signatures, are skipped and do not fail the file. A non-related attachment of a disallowed type fails the whole file as FileNotProcessed, and you may get NoLineItemsExtracted if nothing else was readable.
  • Otherwise put the body in prompt and attach the purchase-order files as files parts.
outputType still comes from choose-output.md, not from words like "quote" in the subject. A business rule may map those words. That rule lives in the adapter.

Replies

Match In-Reply-To and References to a Message-ID you already stored.
  • A match means this mail is about an entity you already created. Update that Cart or Shopping List with its own API, or hand the Quote Request to the quote flow. Do not POST to Intake. A second call creates a second entity and the buyer now has two baskets.
  • No match means a new request. Call once.
Do not auto-reply in a way that invites a second email before you have stored an outcome. A "we could not read this, please resend" reply, processed by the same mailbox rule, is how duplicate Carts get created. Reply with the specific missing fact (MissingCountry, unmatched SKUs from a ProductsNotFound warning) or with the created id.
handle-outcome.md

Handle the outcome

Success is 201 with AgentResponsesSuccess: entityType, entity (the Cart, Quote Request, or Shopping List in REST form), threadId, optional warnings, and cartId only for QuoteRequest. warnings is omitted when there is nothing to report. An empty array is not the success shape.
Other errors use AgentResponsesErrorResponse: statusCode, message, errors, and threadId once the run has started. Parsing failures, before the run starts, have no threadId. Auth failures are a different envelope. See call-responses.md.
Write the outcome back to the channel (email reply or ticket comment) using the code, not a generic "something went wrong". Include threadId whenever it is present. It is the id support can find the run with.

201 with warnings

The entity exists. Store it. Do not call again to "fill the gaps". That creates a second entity.

Warning codeWhat it meansWhat to do
ProductsNotFoundSome requested products were not matched, or are not in the bound Store. products lists the identifiers. The matched lines are on the entity.Tell the channel which identifiers were dropped. A person edits the entity or replies with a better identifier. Apply that as an update action.
FileNotProcessedOne upload could not be read. fileName names it. Other input may still have produced the entity.Tell the channel which file was skipped. If the purchase order was the skipped file, the entity may be incomplete. Review it before fulfilling.
ProductsNotFound as a 400 is the other case: nothing matched, and no entity was created. Same code, different status. Branch on the HTTP status.

400, nothing was created

Safe to correct and send one new attempt, after you have stored that this channel event has no entity.id.
CodeFix in the adapter
RequiredFieldA request field is missing. outputType and locale are the usual ones. A JSON body also needs prompt.
MissingCustomerEmailThe input never stated an email. Add Customer email: … to the prompt.
InvalidInputMessage is No customer found with email "…". The address was stated and matches no Customer. Create the Customer, then allow one new attempt.
MissingCountryCart or Quote Request only. Add Shipping country: XX. locale will not do this.
MissingShoppingListNameShopping List only. State the name in the prompt.
MissingEntityTypeYou called without a resolved outputType. Choose one. Do not retry hoping the text implies it.
NoLineItemsExtractedNo product reference in the text or the files. Often a FileNotProcessed on the only document. Fix the file and include a line in the prompt if you have one.
ProductsNotFoundEvery referenced product missed the catalog. No entity. Surface the identifiers. Do not retry unchanged.
OutOfScopeThe input had none of the cart facts (products, email, and country). The message is not an order request. Do not retry.
BusinessUnitUnresolvedName or pin a unit this Customer belongs to. A pinned unit they are not associated with produces this too.
BusinessUnitAmbiguousSeveral units, none selected. Pass businessUnit.
BusinessUnitLimitExceededToo many units to list. Pass businessUnit.
StoreUnresolvedName or pin a Store this Customer can order from.
StoreAmbiguousSeveral stores. Pass store.
StoreDistributionChannelsUnsupportedThe Store has more than one distribution channel. Choose another Store.
InvalidFieldA multipart part was not named files or payload, or a field failed validation.
FeatureDisabledIntake is not enabled for the Project. This is a configuration problem, not a retry.

A Cart may already exist

QuoteRequestCreationFailed means the Cart was created and the Quote Request was not. The error includes cartId. Store that id against the channel event. Then create the Quote Request from that Cart, or delete the Cart. Deleting it removes customer data. Show the exact proposed deletion request and obtain the user's explicit confirmation before executing it. A second Intake call creates another Cart and leaves the first one behind.
ShoppingListCreationFailed leaves no Cart. It is Shopping List only, including the case where every requested product was dropped because the Store does not carry them.

Nothing was written

ExtractionFailed means the run failed before a commercetools write. Branch on the code. One retry is reasonable when the channel event still has no stored entity id.
ProductSearchNotEnabled is returned until Product Search is enabled for the Project. Retrying without that change fails the same way. Both codes are listed with the Intake errors.

After a 201

Keep entity.id. For a Quote Request also keep cartId. Later channel messages update that resource:
  • Cart: update actions, then freeze, order, or create a Quote Request from it.
  • Quote Request: the quote workflow. The buyer-facing entity already exists.
  • Shopping List: update actions, then add its line items to a Cart when the buyer is ready.

Deleting the entity and starting over is a new Intake call, and it needs a new channel event id or an explicit "retry allowed" flag. An automatic retry of the original event is how the deleted entity comes back as a duplicate.

zendesk.md

Zendesk

When to call

Call Intake when a ticket is a new order request, not when it changes. A trigger (Zendesk's recommended way to send webhooks for ticket activity) with conditions such as "ticket is created" and a tag like order-request is the right gate. ticket.comment_added, or a trigger on every update, creates a new commercetools entity per comment.
The idempotency key is the ticket id. Zendesk redelivers webhooks that do not acknowledge promptly, and the Intake call is slow. Use the same sequence as call-responses.md:
  1. Conditionally insert the ticket id with the normalized request, as queued work that has not been sent. If that insert does not win — the id is already queued, a call is already in an unknown state, or the id already has an entity.id — acknowledge the duplicate so Zendesk stops redelivering, and stop. Do not call from this delivery.
  2. Acknowledge the webhook. The queued request is now the only copy that can still run.
  3. Mark that row as a call with an unknown outcome immediately before the POST, then call Intake once, out of band. Recovery may send a queued row that was never marked. It must not send a row already marked.

Acknowledging before the insert lets two overlapping deliveries both see an empty key and both POST. Acknowledging a bare in-progress marker, with no stored request, drops the ticket if the process exits before the POST.

What to put in the request

The Customer email is the requester's email, looked up from the requester id. It is not the agent, and it is not the address Zendesk uses to send notifications. Write it into the prompt. Do not upload Zendesk's notification mail as an .eml. Its From is the wrong Customer.
Customer email: jane.doe@example.com.
Shipping country: DE.
For a Shopping List, add a third line, Create a shopping list called "PO 4481", using the ticket subject when the description never names the list. Omit the country line for a Shopping List. The ticket description follows the preamble. See choose-output.md.
locale is the ticket's language, not a guess from the SKUs.
When a Zendesk organization is mapped to a commercetools Business Unit, pass that unit's key as businessUnit. The same for Store. The external organization id is not a commercetools key. A unit the Customer is not associated with fails with BusinessUnitUnresolved.

Attachments

The ticket-created payload is not a reliable list of files. Read the ticket's comments and take each attachment's content_url, content_type, and file_name. Download the file and re-upload it as a files part. content_url may be hosted outside Zendesk. Do not send Zendesk credentials to that host.
Skip inline images and any type Intake does not accept. Keep at most 3 files, 20 MB each, preferring the purchase order. See call-responses.md.

Write the result onto the ticket

On 201, add an internal comment (public: false) that a merchant can read: entityType, entity.id, threadId, cartId for a Quote Request, and any warning codes with the unmatched product identifiers. Store entity.id and entityType on ticket custom fields. The comment is for humans. The fields are what the next delivery checks.
On a 400, comment the specific codes (MissingCountry, MissingCustomerEmail, ProductsNotFound, BusinessUnitAmbiguous) and the threadId if present. Do not close the loop by calling Intake again with the same body. Either a person supplies the missing fact and you allow one new attempt, or the ticket stays with the agent.

Later comments

Once the custom field holds an entity id, further comments on that ticket are updates to that entity: Cart update actions, Shopping List update actions, or the quote flow for a Quote Request. They are not new Intake calls.

A genuinely new order from the same requester is a new ticket, which gets its own call.