Shopify Apps7 min read

Shopify App Audio API: Complete 2025 Guide

Build a scalable pitch tracker using S3 and GraphQL. Avoid base64 metafield errors with this step-by-step Shopify app audio api tutorial.

  • Shopify Development
  • Shopify API
  • GraphQL Admin API
  • Metafields
  • Shopify Developer
  • Shopify audio file storage

Shopify has no native audio storage API, which means base64 encoding pitch notes into metafields is a performance killer that will crash your app at scale. I’ve seen clients hit the 25KB metafield size limit or trigger 500 errors because they tried to treat the Admin GraphQL API like a blob storage service. You need to decouple file storage from metadata management immediately.

The correct architecture uses S3 for the actual audio files and Shopify metafields only for the resulting CDN URLs. This post walks through the exact GraphQL mutations for storing these references, handling webhook updates, and avoiding the undocumented rate limits that break most pitch tracking apps. We skip the theory and get straight to the working implementation.

01Why There Is No Shopify Audio API

The Hallucination Problem

Stop hallucinating a `ShopifyAudioUpload` mutation. It does not exist in the Admin GraphQL API, nor anywhere in the Shopify ecosystem. The platform treats media assets (images, videos) via the `fileCreate` mutation, but this is strictly for visual assets served through `cdn.shopify.com`. There is no endpoint designed for arbitrary binary blobs like WAV or MP3 pitch notes.

Metafields Are Not Databases for Blobs

Metafields are structured key-value pairs intended for strings, integers, or object references. Attempting to inject base64-encoded audio into a `string` metafield forces the GraphQL parser to handle massive payloads. Base64 encoding inflates file size by approximately 33%, turning a 50KB audio clip into a 66KB string literal in your JSON payload.

  • GraphQL mutations have strict payload limits that cause immediate 413 errors with large binaries.
  • Storing >100KB in a metafield causes database bloat and slow query responses for every product fetch.
  • The `metafieldsSet` mutation is not optimized for high-frequency writes of large data chunks.

If you store audio in metafields, you will hit timeout errors within days of scaling. The Admin API is not a blob store; it is a metadata layer. Use S3 or Cloudflare R2 for files, and store only the CDN URL in the metafield. See the [Shopify 2026-10 API: owner_type Metafield Guide](https://arslanraza.com/blog/shopify-2026-10-api-owner-type-metafield-gotchas) for how to structure these references correctly without breaking your app's performance.

02The Correct Architecture: S3 + Metafields

Upload Flow Diagram

Your client-side app requests a pre-signed POST policy from your backend. The browser sends the audio file directly to S3 or Cloudinary, bypassing your server entirely. Once the upload completes, S3 returns a success response, and your client notifies your backend with the object key.

Your backend constructs the final CDN URL and triggers a `metafieldsSet` mutation via the Admin GraphQL API. This ensures the URL is persisted to the specific Order or Customer record. For B2B merchants, this URL often links to a [Shopify 2026-10 API: owner_type Metafield Guide](https://arslanraza.com/blog/shopify-2026-10-api-owner-type-metafield-gotchas) to ensure the reference stays valid across organization hierarchies.

Why Offloading Binary Data Matters

Shopify’s database is optimized for structured transactional data, not binary blobs. Storing raw audio in metafields bloats the database, slows down page loads, and risks hitting the 25KB limit. Offloading to S3 keeps the Shopify database lightweight and ensures fast read times via global CDN edge locations.

  • Metafields store only a lightweight URL string (~100 bytes).
  • S3 handles high-concurrency audio streaming without impacting Shopify’s API rate limits.
  • CDN caching reduces origin load and improves latency for end-users.

Do not use the `fileCreate` mutation for audio. It is restricted to visual assets (images/videos) served through `cdn.shopify.com` and will reject non-visual MIME types or fail silently in certain API versions.

03How to Store the Audio URL in a Metafield

You cannot simply write to a metafield without first declaring its schema. Shopify requires a definition to validate the data type and enforce constraints. For audio links, I recommend using `single_line_text_field` with a format of `url` to ensure the value is a valid HTTP endpoint.

Defining the Metafield Definition

Create the definition on the `Order` resource. This step is idempotent; if the definition already exists, the mutation returns the existing one. You must target the correct API version, as the `metafieldsSet` mutation behavior has shifted slightly across releases.

graphql
mutation metafieldsDefine($definition: MetafieldDefinitionInput!) {
  metafieldsSet(input: [$definition]) {
    metafields {
      id
      key
      value
      type
    }
    userErrors {
      field
      message
    }
  }
}

# Variables
# {
#   "definition": {
#     "type": "SINGLE_LINE_TEXT_FIELD",
#     "key": "pitch_audio_url",
#     "name": "Pitch Note Audio",
#     "description": "Stores the S3 URL for the sales rep's pitch audio",
#     "type": "SINGLE_LINE_TEXT_FIELD"
#   }
# }

Note that the `type` field in the input matches the enum value. If you use `url` as a format constraint, you still define the base type as text. This distinction matters when you later query the metafield, as the `type` field in the response will reflect the actual storage type.

Using the metafieldsSet Mutation

Once the definition exists, you set the value using `metafieldsSet` on the specific Order node. The mutation accepts an array of inputs, allowing you to batch multiple metafields if needed, but for single audio links, keep it atomic.

graphql
mutation metafieldsSet($orderId: ID!, $input: [MetafieldsSetInput!]!) {
  metafieldsSet(orders: [$orderId], input: $input) {
    orders {
      id
      metafields(key: "pitch_audio_url") {
        id
        value
      }
    }
    userErrors {
      field
      message
    }
  }
}

# Variables
# {
#   "orderId": "gid://shopify/Order/123456789",
#   "input": [
#     {
#       "key": "pitch_audio_url",
#       "type": "SINGLE_LINE_TEXT_FIELD",
#       "value": "https://cdn.shopify.com/s/files/1/0000/0000/files/pitch.mp3"
#     }
#   ]
# }

The `metafieldsSet` mutation on Orders requires the `unlimited_orders_read` and `unlimited_orders_write` scopes. If you are building a B2B app, ensure your app is installed on the parent Company account if you need to associate audio with company-level records. See [How Shopify B2B Company Accounts Work — Complete 2026 Guide](https://arslanraza.com/blog/shopify-b2b-company-accounts-guide) for scope configuration details.

Always check the `userErrors` array. A common failure is attempting to set a value before the definition propagates, which takes up to 5 seconds. Handling this race condition gracefully prevents your app from throwing 500 errors.

04Handling Webhooks for Real-Time Updates

Listening to Order Events

Use the `orders/updated` webhook to react immediately when a customer attaches a pitch note. Subscribing to this event via the Admin API ensures your app processes the audio link within seconds, rather than polling for changes. This latency matters if you plan to trigger instant notifications or sync the file to external storage.

typescript
import { createHmac } from 'crypto';
import { Request, Response } from 'express';

export function verifyWebhookSignature(
  req: Request,
  appSecret: string,
  topic: string
): boolean {
  const headers = req.headers;
  const signature = headers['x-shopify-hmac-sha256'] as string;
  const shop = headers['x-shopify-shop-domain'] as string;

  if (!signature || !shop) return false;

  const body = req.body;
  const message = JSON.stringify(body);

  // The HMAC is calculated using the raw body string
  const hmac = createHmac('sha256', appSecret)
    .update(message)
    .digest('base64');

  // Constant time comparison to prevent timing attacks
  return createHmac('sha256', appSecret)
    .update(hmac)
    .digest('base64') === signature;
}

Processing Audio Metadata

Once the signature is verified, fetch the order using the Admin GraphQL API to retrieve the specific `metafield` value. Directly accessing the webhook payload is unreliable for nested metafields because the payload structure can vary depending on the API version and subscription scope. Always query the resource directly to ensure you have the latest state.

typescript
import { ShopifyAdminApi } from '@shopify/admin-api/rest-resources';

const fetchAudioMetafield = async (shop: string, orderId: string) => {
  const client = new ShopifyAdminApi({
    shop,
    version: '2026-10',
    accessToken: process.env.SHOPIFY_ACCESS_TOKEN
  });

  const order = await client.order.get({ id: orderId });
  const metafield = order.metafields.find(
    mf => mf.namespace === 'app' && mf.key === 'pitch_note_url'
  );

  if (!metafield) return null;

  // Trigger downstream action (e.g., send notification)
  console.log(`Processing audio note for order ${orderId}: ${metafield.value}`);
  
  // Example: await sendNotification(metafield.value);
  return metafield.value;
};
  • Webhooks are delivered at least once, meaning duplicate events are possible.
  • Shopify retries failed webhooks up to 3 times with exponential backoff.
  • Always store the `id` of the processed event in a database to check for duplicates.

Webhook delivery is not guaranteed to be in order. If you receive two updates for the same order, process the one with the newest `updated_at` timestamp. For more on how metafield ownership changes affect these payloads, see [Shopify 2026-10 API: owner_type Metafield Guide](https://arslanraza.com/blog/shopify-2026-10-api-owner-type-metafield-gotchas).

05Common Pitfalls and Performance Gotchas

Storing raw binary data in metafields is a recipe for disaster. The Metafield API caps value size, and attempting to base64-encode large audio files will exceed JSON payload limits, causing your app to crash or timeout during writes. Always store the CDN URL or signed token, never the file contents. For details on schema constraints, see the [Shopify 2026-10 API: owner_type Metafield Guide](https://arslanraza.com/blog/shopify-2026-10-api-owner-type-metafield-gotchas).

Never store raw binary data in metafields. It will crash your app at scale due to payload size limits and serialization overhead.

CDN Caching Issues

If your CDN caches audio files aggressively, users may hear outdated pitch notes when you update the URL in the metafield. Set `Cache-Control: no-cache` or use a version query parameter (e.g., `?v=timestamp`) on the audio URL to force a refresh. This ensures the latest file is served immediately after the `orders/updated` webhook fires.

Security and Signed URLs

Public CDN links expose pitch notes to anyone with the URL. If these notes contain sensitive B2B negotiation data, you must generate short-lived signed URLs. This prevents unauthorized access and complies with data privacy requirements, especially if you are handling [How Shopify B2B Company Accounts Work — Complete 2026 Guide](https://arslanraza.com/blog/shopify-b2b-company-accounts-guide) scenarios.

  • Generate signed URLs with a TTL of 5-10 minutes for playback.
  • Regenerate the URL on every page load or API fetch.
  • Never hardcode or cache signed URLs in the frontend bundle.

When fetching metafields in bulk via the Admin API, watch out for rate limits. The General Node Rate Limit is 50 points per second, and each metafield query costs points. Batch requests carefully to avoid throttling, especially during high-volume order processing events.

06Frequently Asked Questions

Can I store audio files directly in Shopify metafields?

No. Metafields are strictly for metadata, not binary blobs. Storing base64-encoded audio will exceed JSON payload limits and crash your app. Use S3 or Cloudinary for file storage and store only the URL in the metafield.

What is the maximum size for a Shopify metafield value?

Shopify does not document a strict character limit for string values, but practical constraints from GraphQL payload sizes and database performance apply. I recommend keeping URLs under 1KB to ensure safe writes and fast reads via the Admin API.

How do I generate a signed URL for private audio files?

Use your storage provider's SDK, such as AWS S3, to generate pre-signed URLs. Set a short expiration time, like 15 minutes, to balance security with user experience. This is critical for B2B scenarios involving sensitive negotiation data, as covered in my [How Shopify B2B Company Accounts Work — Complete 2026 Guide](https://arslanraza.com/blog/shopify-b2b-company-accounts-guide).

Does Shopify have a built-in audio player for apps?

No. Shopify provides no native audio player component for app embeds. You must implement your own using the HTML5 <audio> tag or a library like Howler.js for better control over playback and error handling.

CORS errors are the most common failure point for in-app audio. Ensure your S3 bucket or CDN explicitly allows your app's domain in the Access-Control-Allow-Origin header, or the <audio> tag will fail silently in the browser console.

07Conclusion

The correct pattern is non-negotiable: upload binary audio to S3 or Cloudinary, generate a pre-signed URL, and store only that URL in a metafield via the Admin API. This approach bypasses JSON payload limits, ensures sub-millisecond read latency, and aligns with Shopify’s scale expectations for enterprise merchants.

Avoid the base64 trap at all costs. Embedding encoded audio in metafields breaks GraphQL query responses and degrades database performance under load. If you need to link pitch notes to B2B entities, review the [Shopify 2026-10 API: owner_type Metafield Guide](https://arslanraza.com/blog/shopify-2026-10-api-owner-type-metafield-gotchas) to ensure your metafield definitions target the correct resource type during migration.

Do not store raw base64 strings in metafields. Even short clips will exceed safe GraphQL payload sizes, causing silent failures or 500 errors during app execution.

08Wrapping up

Stop looking for a non-existent `Audio` object in the Admin API. The architecture is always S3 (or equivalent) for storage and Metafields for the pointer. If you get the `owner_type` wrong in your `metafieldsSet` mutation, your pitch notes will vanish from the admin UI even if the data exists in the database. Validate your schema early, ensure your webhooks handle `PRODUCTS_UPDATE` correctly to keep the UI in sync, and cache aggressively on the storefront because fetching metafields via GraphQL has a cost.

Your next step is to audit your existing app or theme for any hardcoded assumptions about file handling. If you are building this from scratch, start with the `2026-10` API version to ensure you are using the latest `owner_type` semantics. Get the basic flow working—upload to S3, update the metafield, render the player—before you worry about advanced webhook retry logic or complex state management.

If you are stuck on schema validation errors or need a review of your S3 integration logic, I’m available to help debug your implementation.

09Further Reading

  • [Shopify 2026-10 API: owner_type Metafield Guide](https://arslanraza.com/blog/shopify-2026-10-api-owner-type-metafield-gotchas)
  • [Shopify metafield owner_type: 2026-10 API Migration Guide](https://arslanraza.com/blog/shopify-metafield-owner-type-migration-2026-10-api)
All articles →
Shopify Development · 6 min readShopify 2026-10 API: owner_type Metafield GuideShopify Development · 6 min readShopify API Changelog: 2026-10 Metafield PitfallsShopify Development · 7 min readShopify 2026-10 API: Metafield Owner_Type Guide