Skip to main content
- Endpoint reference

API endpoint reference

These are the customer-facing endpoints in the first API version. All paths are relative to your API base URL, which the app shows under Settings → API keys once you have created a key.

Assets

MethodPathPurpose
POST/assetsImport a remote URL as an asset.
POST/assets/uploadsBegin a direct upload.
POST/assets/uploads/{uploadId}/completeFinalize a direct upload.
GET/assetsList assets.
GET/PATCH/DELETE/assets/{id}Read, update, or delete an asset.

Categories, mappings, packs, links, and tags

MethodPathPurpose
GET/POST/categoriesList or create flat categories.
GET/PATCH/DELETE/categories/{id}Read, rename, or delete a category.
GET/POST/mappingsList or upsert Shopify product mappings.
GET/PUT/DELETE/mappings/{id}Read, replace, or delete a mapping.
GET/POST/packsList or create packs.
GET/DELETE/packs/{id}Read or delete a pack.
GET/POST/linksList or create links. Creating requires a paid plan; visit limits and expiry stay admin-only.
GET/POST/license-key-tagsList or create license-key tags.

POST /mappings and PUT /mappings/{id} accept an optional notify_customers object ({}, or { "message": "…", "variant_ids": [ … ] }). After the write commits, Alva queues a product update notification for the buyers of the mapping you wrote — the same campaign as POST /products/{shopify_product_id}/notifications below, with the same note rules and the same 7-day limit per product. Only the checks on the note's own text run before the write: a note that is blank, too long, uses an unknown or malformed variable, names more than five metafields, or has a link in its text is 422 invalid_message and nothing is written. Every other refusal is reported inside the response as data.notification with created: false and an error code — never as a 4xx, so the mapping write itself still succeeds. That includes a metafield problem (invalid_message with errors[]: a metafield that is missing, of an unsupported type, too long or contains a link, or a note too long once its variables are filled in), notification_cooldown with next_available_at, too_many_active_notifications with limit and active_notification_ids, no_eligible_recipients, email_disabled, and archive_not_queued when the file rebuild could not be queued. A brand-new mapping has no buyers yet and reports no_eligible_recipients. data.notification is present only when you sent the flag. The flag adds no rate-limit cost: the mapping write costs its usual 2 tokens. Replaying the same Idempotency-Key returns the stored response without a second campaign; to retry only the notification, call POST /products/{shopify_product_id}/notifications with a fresh key and variant_ids naming the written mapping's variants, or the same variant_ids the flag carried. Without variant_ids that endpoint notifies the buyers of every mapping of the product, not just the one you wrote.

One flag per product, not one per variant. The flag emails only the buyers of the mapping you wrote, but it uses the whole product's 7-day limit. If you write a product's variant mappings one at a time with the flag on each, the first campaign holds the product's 7 days, so the others report notification_cooldown. Put the flag on one write instead, the last one, so the campaign also waits for the rebuilds the earlier writes queued. Name every variant whose buyers should hear about it in variant_ids, including the written mapping's own: variant_ids replaces the default scope rather than adding to it.

Orders and access

MethodPathPurpose
GET/orders/{shopify_order_id}Read order download status.
GET/POST/orders/{shopify_order_id}/assignmentsList or create product-level assignments.
GET/orders/{shopify_order_id}/assignments/{purchaseId}/download-urlsMint download URLs.
POST/orders/{shopify_order_id}/revokeDisable access.
POST/orders/{shopify_order_id}/restoreRestore access.
POST/orders/{shopify_order_id}/grant-downloadsGrant extra downloads.
POST/orders/{shopify_order_id}/resendResend the delivery email.

The download-urls endpoint returns 422 withdrawal_consent_required when the shop has EU withdrawal-right consent enabled and the order has no consent record yet; download URLs are issued once the customer consents.

The download-urls and resend endpoints return 409 delivery_not_yet_released when the product has a download delivery delay configured and the buyer's files have not unlocked yet. The error.details.release_at value is the ISO 8601 instant they unlock, and the grant-downloads endpoint releases the order early.

Product update notifications

Email the buyers of a product that it has been updated — the same campaign a merchant starts with Notify customers on a product's page in the app — from your own system. Available on every paid plan; 403 forbidden means the API is switched off for the app (the global kill switch) or the feature has been switched off by Alva — contact us if you see it. In the path, {shopify_product_id} is the bare numeric product id (recommended) or a GID percent-encoded as one path segment (gid%3A%2F%2Fshopify%2FProduct%2F123); a literal gid://shopify/Product/123 is several path segments and does not reach the route.

MethodPathPurpose
POST/products/{shopify_product_id}/notificationsQueue a campaign for every eligible buyer of the product (202). Send { "dry_run": true } to run the same checks and get the recipient count, the subject and the rendered note without sending anything (200). Costs 10 rate-limit tokens either way.
GET/notifications/{notificationId}Read a campaign: status, counts, waiting_for_archive, error. Campaigns started from the app appear too.
POST/notifications/{notificationId}/cancelCancel a campaign that is still queued (200, status cancelled). The product's 7-day limit is kept.

Request body (every field optional; {} notifies the whole product without a note). message: a plain-text note of up to 2,000 characters, shown as "A note from {your shop}:" above the download button. The note is for describing what changed in the product — corrections, new files, what to download again — and must not be used for promotions or discounts. It may use {customer.first_name}, {shop.name}, {product.title} and {product.metafields.<namespace>.<key>} (up to five metafields of text, number, date or list-of-text type; other types are refused). Links are not allowed anywhere in the note — web addresses, www., bare domains and email addresses are refused, including inside a metafield value — and an unknown variable is refused rather than left in as text. A present-but-blank message is refused; omit the field for no note. variant_ids: limit the campaign to the mappings that serve those variants (bare ids or GIDs, at most 100); omit it for the whole product. An empty array is 400 invalid_request, never "everything". dry_run: see the table.

Who is emailed. By default every mapping of the product that has files, packs or links; keys-only and personalized mappings are skipped. A buyer counts once however many targeted variants they bought, and the button opens their most recent purchase. Buyers who opted out of product update email are skipped and counted in opted_out_count.

One campaign per product per 7 days, across all of its variants and every source — this endpoint, the notify_customers flag on a mapping write, and the merchant's own button. Inside that week, and while a campaign for the product is still queued or sending, the answer is 429 notification_cooldown with Retry-After (seconds, up to seven days) and error.details.next_available_at. This is not the per-key token bucket: do not retry it automatically — a retry loop keyed on 429 will spin for a week. Schedule the next call at next_available_at. Cancelling a campaign does not give the product its 7 days back; use dry_run to check a note before sending. A campaign that fails with zero sends does release the limit, so it can simply be retried.

At most 5 campaigns per shop at once. A shop may have at most 5 campaigns queued or sending at the same time, across all its products and every source. A sixth is 429 too_many_active_notifications: error.details.limit is the cap and error.details.active_notification_ids lists this shop's queued or sending campaigns, oldest first. There is no Retry-After; poll GET /notifications/{id} for one of them and retry once it has finished. A dry run is refused the same way. On a mapping write it is reported in data.notification, never as a 4xx.

One product update email per buyer per day. A buyer gets at most one product update email from a shop in any 24 hours, counted per email address, across every campaign, product and source. This is applied when the campaign sends: a buyer who already got one is skipped, and a skip is not a failure. Skipped buyers are not counted in sent_count, failed_count or suppressed_count, so recipient_count can be higher than their sum. A campaign in which every buyer was skipped ends failed with error: recipients_recently_notified and does not use the product's 7 days. One in which only some were skipped completes as usual and does use them.

The archive wait. If a targeted mapping's ZIP archive is being rebuilt — you just replaced its files — the campaign waits until the rebuild has finished so no buyer downloads the old archive: waiting_for_archive: true on the 202 and on GET. Only a mapping that delivers a ZIP archive makes a campaign wait. A rebuild that fails, or that makes no progress for 60 minutes while building (6 hours while still waiting to start or to retry), counted from the rebuild's last change of state, not from when the campaign was created, fails the campaign with zero sends (error is archive_build_failed or archive_not_ready) and releases the limit; fix the files and retry.

Idempotency. A 202 is replayed for 24 hours under the same key (same id, no second campaign). Refusals are never stored, so a same-key retry after next_available_at is evaluated again. Use a fresh Idempotency-Key per dry run: a replayed key returns the stored preview, and the same key with and without dry_run is 409 idempotency_conflict.

Status on GET. queued → processing → completed, failed or cancelled. completed with partial: true means some sends failed or were suppressed (see sent_count, failed_count, suppressed_count), or sending stopped part-way (below). On failed, error is one of interrupted, worker_crashed, send_aborted, archive_build_failed, archive_not_ready, email_disabled, invalid_message, plan_required, notifications_suspended, notifications_switched_off, nothing_sent, recipients_recently_notified, no_eligible_recipients, failed. source is admin, api or api_mapping.

Checks while sending. Before each batch of up to 500 emails the campaign checks the shop again. It stops if the subscription became inactive or the shop moved to the free plan (plan_required), customer emails were turned off (email_disabled), sending was suspended after complaints (notifications_suspended), or Alva switched notifications off (notifications_switched_off). Stopped before anyone was emailed, the campaign is failed with that error and the product's 7 days are released. Stopped after some buyers were emailed, it is completed with partial: true. nothing_sent means every send failed, so nobody was emailed; it releases the 7 days too. no_eligible_recipients means nobody was left to email by the time the campaign sent (every buyer had opted out or lost download access), so nothing was sent and the 7 days are released.

Refusals. 402 plan_required (the shop is on the Free plan, or its subscription is not active), 403 forbidden (the API is switched off for the app, or the feature has been switched off by Alva — contact us), 403 notifications_suspended (sending is suspended after complaints — contact support), 404 mapping_not_found (no mapping for the product, or a variant that no mapping serves or that Shopify does not list as one of the product's variants — error.details.unresolved_variant_ids), 409 multiple_mappings_match (error.details.ambiguous_variant_ids), 409 personalized_product, 422 invalid_message (error.details.errors[] names each broken rule, error.details.max_length is the cap), 422 email_disabled (customer email is switched off in the app), 422 no_downloadable_content, 422 no_eligible_recipients (error.details.opted_out_count), 429 notification_cooldown, 429 too_many_active_notifications (error.details.limit, error.details.active_notification_ids), 409 app_not_installed (the note names a metafield or you sent variant_ids, and Alva has no active Shopify session for the shop, usually because the app was uninstalled), 500 internal_error (for example Shopify could not return the product's metafields for the note or confirm the requested variants, or the create waited more than 8 seconds behind the shop's other creates: every 500 from this endpoint releases the Idempotency-Key, so retry the same request with the same key; if the failed attempt did queue the campaign after all, the retry is refused with 429 notification_cooldown instead of starting a second one). GET and cancel answer 404 notification_not_found for a missing or foreign id, and cancel answers 409 notification_not_cancellable with error.details.status once a campaign has left the queue. See the errors page for what to do about each.

Health and status

MethodPathPurpose
GET/healthCheck API availability.
GET/pingCheck authentication.
GET/jobs/{jobId}Read async job status.

API docs

Was this helpful?

Still stuck? Email admin@alvaapps.com.

Last updated 2026-10-20