# Connect an Upfirst receptionist to Shopify

How to let an Upfirst AI receptionist check order status and product stock in a
Shopify store. Written for an AI assistant (ChatGPT or Claude) with the Upfirst
connector, setting this up for a business owner.

## Before you start

The owner needs:

- A Shopify app made in the Shopify Dev Dashboard, released and installed on
  their store, with the scopes read_orders, read_inventory and read_products.
  The app's Settings page shows its Client ID and Client secret.
- An Upfirst account with a receptionist and a phone number.
- The Upfirst connector added to this chat:
  - Claude: https://claude.ai/directory/upfirst
  - ChatGPT: https://chatgpt.com/plugins/plugin_asdk_app_6aa057dfb19c81919777fdc34a9d1f12?search=upfirst

If the connector isn't connected, send the owner the right link above and stop
until it is.

You also need the store's address, which ends in .myshopify.com. If the owner
hasn't given it, ask. It's in Shopify under Settings, then Domains.

## Never handle the Client ID or Client secret

Don't ask the owner for their Client ID or Client secret, and don't use them if
they paste them into the chat. Use the placeholders below exactly as written.
The owner pastes the real values into Upfirst themselves at the end. If they do
paste the secret into the chat, suggest they rotate it in the Shopify Dev
Dashboard.

## Step 1: Pick the receptionist

List the owner's agents. If there's more than one, ask which one to use. Use
only that agent's id for everything below.

## Step 2: Check what's already there

Read the agent's custom actions and knowledge. If a custom action or knowledge
entry with the same name as one below already exists, show it to the owner and
ask before changing it. Don't delete anything.

## Step 3: Create three custom actions

Link each one to the chosen agent only. Replace YOUR-STORE in every URL with
the store's address.

### Action 1: Get Shopify Token

Gets a fresh Shopify access token at the start of every call. Shopify's tokens
expire after 24 hours, so this keeps the receptionist from ever using an
expired one.

- Name: Get Shopify Token
- Timing: before (when the call starts)
- Description: Runs at the start of every call to get a fresh Shopify access token. The access_token it returns is used by the Shopify lookup actions. Never read it out or mention it to the caller.
- Method: POST
- URL: https://YOUR-STORE.myshopify.com/admin/oauth/access_token
- Auth type: none
- Headers: Content-Type = application/json
- Body template, exactly:

    {
      "grant_type": "client_credentials",
      "client_id": "PASTE_YOUR_CLIENT_ID_HERE",
      "client_secret": "PASTE_YOUR_CLIENT_SECRET_HERE"
    }

- Allowed output fields: access_token
- Timeout: 3 seconds
- Fallback message: I can't look up stock or orders right now, but I can take a message and have someone call you back.

### Action 2: Check Order Status (Shopify)

- Name: Check Order Status (Shopify)
- Timing: during
- Description: When a caller asks about an existing order, like where it is, whether it shipped, or for a tracking number, ask for their order number, then run this action.
- Method: POST
- URL: https://YOUR-STORE.myshopify.com/admin/api/2026-07/graphql.json
- Auth type: none
- Headers: Content-Type = application/json, and X-Shopify-Access-Token = {{access_token}}
- Variables, both custom (not system) and both required:
  - order_number: The caller's order number, digits only, without the # sign. Example: 1001
  - access_token: The access_token returned by the Get Shopify Token action at the start of this call. Copy it exactly. Never ask the caller for it or say it out loud. Example: shpat_example
- Body template, exactly:

    {
      "query": "query($search: String!) { orders(first: 1, query: $search, sortKey: CREATED_AT, reverse: true) { edges { node { name createdAt displayFinancialStatus displayFulfillmentStatus fulfillments(first: 3) { trackingInfo { number url } } } } } }",
      "variables": { "search": "name:{{order_number}}" }
    }

- Allowed output fields:
  - data.orders.edges[0].node.name
  - data.orders.edges[0].node.createdAt
  - data.orders.edges[0].node.displayFinancialStatus
  - data.orders.edges[0].node.displayFulfillmentStatus
  - data.orders.edges[0].node.fulfillments
- Fallback message: I'm not able to look up orders right now. I can take a message and have someone call you back with an update.

### Action 3: Check Product Stock and Pricing (Shopify)

- Name: Check Product Stock and Pricing (Shopify)
- Timing: during
- Description: When a caller asks whether a product is in stock, what sizes or options it comes in, or how much it costs, ask which product they mean, then run this action. Each option has its own price and stock. If availableForSale is false for an option, that option is sold out.
- Method: POST
- URL: https://YOUR-STORE.myshopify.com/admin/api/2026-07/graphql.json
- Auth type: none
- Headers: Content-Type = application/json, and X-Shopify-Access-Token = {{access_token}}
- Variables, both custom (not system) and both required:
  - product_name: The product the caller is asking about, in a word or two. Example: t-shirt
  - access_token: The access_token returned by the Get Shopify Token action at the start of this call. Copy it exactly. Never ask the caller for it or say it out loud. Example: shpat_example
- Body template, exactly:

    {
      "query": "query($search: String!) { products(first: 3, query: $search) { edges { node { title variants(first: 3) { edges { node { title price inventoryQuantity availableForSale } } } } } } }",
      "variables": { "search": "title:*{{product_name}}*" }
    }

- Allowed output fields: for each product index P from 0 to 2, include
  data.products.edges[P].node.title, and for each variant index V from 0 to 2,
  include these four:
  - data.products.edges[P].node.variants.edges[V].node.title
  - data.products.edges[P].node.variants.edges[V].node.price
  - data.products.edges[P].node.variants.edges[V].node.inventoryQuantity
  - data.products.edges[P].node.variants.edges[V].node.availableForSale

  That's 39 fields. Write them all out with the numbers filled in.
- Fallback message: I'm not able to check stock right now. I can take a message and have someone call you back with the details.

Don't include productwide stock totals (totalInventory). Shopify updates them in
the background, so they can show 0 for a product that's in stock.

## Step 4: Add two knowledge entries

Write them as below. Leave three blank lines before each ### heading inside the
content.

### Knowledge entry: Order Status

When a caller asks about the status of an order they already placed, ask for their order number, then run the **Check Order Status (Shopify)** action with that number.

> "Sure, I can check on that. What's your order number?"

#### They gave an order number

Run **Check Order Status (Shopify)**, then respond based on what comes back:

- **Shipped** (displayFulfillmentStatus is FULFILLED): tell them it's on the way, and offer the tracking number if one is available.
  > "Good news, your order has shipped. Want me to give you the tracking number?"
- **Not shipped yet** (displayFulfillmentStatus is UNFULFILLED or PENDING): tell them it's confirmed but hasn't left yet.
  > "Your order is confirmed, it just hasn't shipped yet."
- **Partly shipped** (displayFulfillmentStatus is PARTIALLY_FULFILLED): tell them part has shipped and the rest is on the way.
- **Payment pending** (displayFinancialStatus is PENDING): mention payment hasn't cleared yet.
- **Refunded** (displayFinancialStatus is REFUNDED): let them know the order was refunded.

#### No order found

Ask them to double-check the number, then try once more.

> "I'm not finding that one. It's usually four digits and may start with a hash sign. Can you read it back to me?"

If it still doesn't come up, take their name and let them know someone will follow up.

#### Always

- Speak in everyday terms: shipped, not yet shipped, refunded. Never read out raw status codes or internal field names.
- Only look up an order after the caller gives a number. Don't guess.

### Knowledge entry: Stock and Pricing

When a caller asks whether we have a product, what sizes or options it comes in, or how much it costs, ask which product they mean if they haven't said, then run the **Check Product Stock and Pricing (Shopify)** action.

#### Reading the result

- Each size or option has its own price and stock. Give the price for the one they asked about, or list the options if they didn't say.
- If an option isn't available for sale, it's sold out. Offer another option if one is in stock.
- If there are 5 or fewer left, mention it.
  > "We have it, but we're down to the last few."
- If nothing matches, ask them to say the name another way and try once more.

Never guess a price or stock count. Only quote what the action returns.

(In the saved entries, use ### for the #### headings above.)

## Step 5: Tell the owner what's left

List what you created. Then tell the owner to do these two things themselves:

1. In Upfirst, go to Custom Actions, open Get Shopify Token, and replace
   PASTE_YOUR_CLIENT_ID_HERE and PASTE_YOUR_CLIENT_SECRET_HERE in the request
   body with the Client ID and Client secret from their Shopify app. Then save.
2. Call the receptionist's number and ask about a real product and a real order
   number. If the store has no orders yet, create a test order in Shopify first.

## Troubleshooting

- **The receptionist says it can't check stock or orders right now.** The token
  request failed. Check that the real Client ID and Client secret replaced the
  placeholders, and that the app is installed on the store.
- **It can't find an order.** Shopify order numbers usually start at 1001. Check
  the caller gave the number without extra digits.
- **It can't find a product.** It searches product titles, so nicknames may not
  match. Try the name as it appears in Shopify.
- **Lookups fail but the token works.** Check both lookup actions send
  X-Shopify-Access-Token = {{access_token}}, with no spaces inside the braces.

More help: https://upfirst.ai/blog/ai-receptionist-for-shopify or email support@upfirst.ai
