Shop settings, integrations, domain, SMTP
/admin/api/shop-info
Returns the shop name. No authentication required -- used by the admin login page.
/admin/api/settings
Returns current shop settings including name, contact email, theme colour, language, currency, tax, and country configuration.
/admin/api/settings
Update shop settings. The name and theme_color fields are required; all others use COALESCE semantics (omit to keep existing value).
/admin/api/settings/log-404s
Returns whether storefront 404s are being recorded into the not-found log.
/admin/api/settings/log-404s
Enable or disable recording of storefront 404s. Existing log entries are unaffected when the toggle is flipped off; the storefront simply stops appending new ones.
/admin/api/available-languages
Returns the full list of languages available for selection. Each entry has a BCP 47 code and a native display name.
/admin/api/custom-domain
Returns the shop's current custom domain, or null if none is configured.
/admin/api/custom-domain
Set or clear the shop's custom domain. Must contain at least one dot. Must not be a subdomain of the platform domain. Send null to clear. Requires a Live-tier shop — Preview shops receive 402 with `error: tier_limit_exceeded`, `reason: custom_domain_requires_live_tier`. Clearing the domain (setting null) is always allowed. See the Tenant Tier reference.
/admin/api/custom-scripts
Returns the current custom JavaScript/HTML snippets injected into the storefront.
/admin/api/custom-scripts
Update the custom scripts injected into the storefront's head and body.
/admin/api/smtp
Returns current SMTP settings. The password is never returned -- only a has_password boolean.
/admin/api/smtp
Update SMTP settings. Password is encrypted at rest with AES-256-GCM. Send null for password to preserve existing. When enabling, host, sender_email, and password are required.
/admin/api/smtp/test
Send a test email to the shop's contact email address using the saved SMTP settings. SMTP must be enabled and fully configured.
/admin/api/integrations
List all configured integrations (payment gateways, marketing tools, shipping providers), ordered by category then provider.
/admin/api/integrations/{provider}
Get a single integration by provider name.
/admin/api/integrations/{provider}
Create or update an integration. Cannot disable the last active payment method.
/admin/api/integrations/stripe-connect/create-account
Create a Stripe Express Connected Account for this shop. Only one account can be connected at a time.
/admin/api/integrations/stripe-connect/onboarding-link
Generate a Stripe Account Link for merchant onboarding. The merchant is redirected to Stripe to enter business details and bank info.
/admin/api/integrations/stripe-connect/status
Refresh the connected Stripe account's status by calling the Stripe API. Updates cached charges_enabled and details_submitted flags.
/admin/api/integrations/stripe-connect/disconnect
Remove the connected Stripe account. The account is not deleted on Stripe's side. Cannot disconnect if it is the last active payment method.
/admin/api/integrations/easyship/test
Test the Easyship API connection by sending a minimal rates request with the configured API key and origin address.
/admin/api/marketing/channels
Returns first-touch and last-touch revenue/order counts grouped by UTM source, UTM campaign, and HTTP referrer. Used for the order-attribution report. Test orders are excluded.
/admin/api/seo/static-pages
List SEO meta for all eight built-in storefront routes.
/admin/api/seo/static-pages/{route}
Fetch SEO meta for a single built-in route.
/admin/api/seo/static-pages/{route}
Set SEO meta for a built-in route. Empty strings fall back to template defaults; pass `og_image_media_id: null` to clear.
/admin/api/seo/static-pages/{route}/translations
List per-locale overrides for a static page's meta_title and meta_description.
/admin/api/seo/static-pages/{route}/translations
Set per-locale overrides for meta_title and meta_description. Replaces all overrides for the route.
/admin/api/custom-domain/status
Returns the current custom-domain provisioning status as a tagged object keyed by `state` (one of none, pending_dns, pending_ssl, active, failed). For any non-none state the payload includes the CNAME records to add at the registrar and the raw Cloudflare ssl_status. Same shape and permission as GET /admin/api/custom-domain; exists so the setup wizard can poll while the domain is in a non-terminal state.
/admin/api/settings/favicon
Upload the shop favicon. Multipart/form-data with a single `file` field (PNG or JPEG, source ≤ 2 MB). Generates 32px and 180px PNGs plus stores the source, then returns cache-busted public URLs. Tier-gated by per-file size only.
/admin/api/settings/checkout-logo
Upload the checkout-page logo. Multipart/form-data with a single `file` field (PNG or JPEG, source ≤ 2 MB). The image is downscaled to a PNG, stored, and the cache-busted public URL returned. Tier-gated by per-file size only.
/admin/api/integrations/stripe-connect/dashboard-link
Create a one-time Stripe Express Dashboard login link for the connected account. Redirect the merchant to the returned URL to view payouts and balances on Stripe's hosted dashboard. The link is single-use and short-lived; request it on demand.
/admin/api/apple-pay/status
Returns the cached Apple Pay domain-registration status from the registry without calling Stripe. Cheap enough to call on every Payments page render. status is one of active, inactive, pending, or null when no registration exists; status_reason carries Stripe's reason when not active.
/admin/api/apple-pay/recheck
Force a fresh Stripe round-trip to re-register and re-check the Apple Pay domain under the per-tenant reconcile lock, then return the refreshed cached row. Powers the SPA's "Re-check" button. Errors from the underlying reconcile are logged and swallowed -- the response always returns whatever the registry currently holds, so the same payload shape as GET status is returned even if Stripe is momentarily down. Takes no body.
/admin/api/shop-info
Returns the shop name. No authentication required -- used by the admin login page.
{
"name": "My Awesome Shop"
}
curl "https://yourshop.zeroshop.io/admin/api/shop-info"
import requests
resp = requests.get("https://yourshop.zeroshop.io/admin/api/shop-info")
info = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shop-info");
const info = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shop-info")
resp = Net::HTTP.get_response(uri)
data = JSON.parse(resp.body)
/admin/api/settings
Returns current shop settings including name, contact email, theme colour, language, currency, tax, and country configuration.
{
"name": "My Awesome Shop",
"contact_email": "hello@myshop.com",
"theme_color": "#3b82f6",
"default_language": "en",
"supported_languages": [
"en",
"sv"
],
"currency_code": "USD",
"currency_symbol": "$",
"prices_include_tax": false,
"allowed_countries": [
"US",
"CA",
"GB"
],
"tax_provider": "builtin"
}
curl "https://yourshop.zeroshop.io/admin/api/settings" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/settings",
headers={"Authorization": "Bearer zspat_..."}
)
settings = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/settings",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const settings = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/settings")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/settings
Update shop settings. The name and theme_color fields are required; all others use COALESCE semantics (omit to keep existing value).
{
"name": "My Awesome Shop",
"contact_email": "hello@myshop.com",
"theme_color": "#3b82f6",
"default_language": "en",
"supported_languages": [
"en",
"sv"
],
"currency_code": "USD",
"currency_symbol": "$",
"prices_include_tax": false,
"allowed_countries": [
"US",
"CA"
],
"tax_provider": "builtin"
}
curl -X PATCH "https://yourshop.zeroshop.io/admin/api/settings" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"name": "My Awesome Shop", "theme_color": "#3b82f6", "currency_code": "USD", "currency_symbol": "$"}'
import requests
resp = requests.patch(
"https://yourshop.zeroshop.io/admin/api/settings",
headers={"Authorization": "Bearer zspat_..."},
json={
"name": "My Awesome Shop",
"theme_color": "#3b82f6",
"currency_code": "USD",
"currency_symbol": "$"
}
)
assert resp.status_code == 204
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/settings",
{
method: "PATCH",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "My Awesome Shop",
theme_color: "#3b82f6",
currency_code: "USD",
currency_symbol: "$"
})
}
);
// 204 No Content
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/settings")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { name: "My Awesome Shop", theme_color: "#3b82f6" }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
# 204 No Content
/admin/api/settings/log-404s
Returns whether storefront 404s are being recorded into the not-found log.
{
"enabled": true
}
curl "https://yourshop.zeroshop.io/admin/api/settings/log-404s" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/settings/log-404s",
headers={"Authorization": "Bearer zspat_..."}
)
toggle = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/settings/log-404s",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const toggle = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/settings/log-404s")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/settings/log-404s
Enable or disable recording of storefront 404s. Existing log entries are unaffected when the toggle is flipped off; the storefront simply stops appending new ones.
{
"enabled": true
}
{
"enabled": true
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/settings/log-404s" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/settings/log-404s",
headers={"Authorization": "Bearer zspat_..."},
json={"enabled": True}
)
toggle = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/settings/log-404s",
{
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ enabled: true })
}
);
const toggle = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/settings/log-404s")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { enabled: true }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/available-languages
Returns the full list of languages available for selection. Each entry has a BCP 47 code and a native display name.
[
{
"code": "en",
"name": "English"
},
{
"code": "sv",
"name": "Svenska"
},
{
"code": "de",
"name": "Deutsch"
},
{
"code": "fr",
"name": "Français"
}
]
curl "https://yourshop.zeroshop.io/admin/api/available-languages" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/available-languages",
headers={"Authorization": "Bearer zspat_..."}
)
languages = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/available-languages",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const languages = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/available-languages")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/custom-domain
Returns the shop's current custom domain, or null if none is configured.
{
"domain": "shop.acme.com"
}
curl "https://yourshop.zeroshop.io/admin/api/custom-domain" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/custom-domain",
headers={"Authorization": "Bearer zspat_..."}
)
data = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/custom-domain",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const data = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/custom-domain")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/custom-domain
Set or clear the shop's custom domain. Must contain at least one dot. Must not be a subdomain of the platform domain. Send null to clear. Requires a Live-tier shop — Preview shops receive 402 with `error: tier_limit_exceeded`, `reason: custom_domain_requires_live_tier`. Clearing the domain (setting null) is always allowed. See the Tenant Tier reference.
{
"domain": "shop.acme.com"
}
{
"domain": "shop.acme.com"
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/custom-domain" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"domain": "shop.acme.com"}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/custom-domain",
headers={"Authorization": "Bearer zspat_..."},
json={"domain": "shop.acme.com"}
)
data = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/custom-domain",
{
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ domain: "shop.acme.com" })
}
);
const data = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/custom-domain")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { domain: "shop.acme.com" }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/custom-scripts
Returns the current custom JavaScript/HTML snippets injected into the storefront.
{
"head_scripts": "<!-- Google Analytics -->\n<script async src=\"https://www.googletagmanager.com/gtag/js?id=G-XXXXX\"></script>",
"body_scripts": ""
}
curl "https://yourshop.zeroshop.io/admin/api/custom-scripts" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/custom-scripts",
headers={"Authorization": "Bearer zspat_..."}
)
scripts = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/custom-scripts",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const scripts = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/custom-scripts")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/custom-scripts
Update the custom scripts injected into the storefront's head and body.
{
"head_scripts": "<script async src=\"https://www.googletagmanager.com/gtag/js?id=G-XXXXX\"></script>",
"body_scripts": ""
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/custom-scripts" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"head_scripts": "<script>...</script>", "body_scripts": ""}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/custom-scripts",
headers={"Authorization": "Bearer zspat_..."},
json={"head_scripts": "<script>...</script>", "body_scripts": ""}
)
assert resp.status_code == 204
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/custom-scripts",
{
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ head_scripts: "<script>...</script>", body_scripts: "" })
}
);
// 204 No Content
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/custom-scripts")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { head_scripts: "<script>...</script>", body_scripts: "" }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
# 204 No Content
/admin/api/smtp
Returns current SMTP settings. The password is never returned -- only a has_password boolean.
{
"enabled": true,
"host": "smtp.mailgun.org",
"port": 587,
"username": "postmaster@mg.myshop.com",
"has_password": true,
"encryption": "starttls",
"sender_email": "noreply@myshop.com",
"sender_name": "My Awesome Shop"
}
curl "https://yourshop.zeroshop.io/admin/api/smtp" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/smtp",
headers={"Authorization": "Bearer zspat_..."}
)
smtp = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/smtp",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const smtp = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/smtp")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/smtp
Update SMTP settings. Password is encrypted at rest with AES-256-GCM. Send null for password to preserve existing. When enabling, host, sender_email, and password are required.
{
"enabled": true,
"host": "smtp.mailgun.org",
"port": 587,
"username": "postmaster@mg.myshop.com",
"password": "secret-smtp-password",
"encryption": "starttls",
"sender_email": "noreply@myshop.com",
"sender_name": "My Awesome Shop"
}
curl -X PATCH "https://yourshop.zeroshop.io/admin/api/smtp" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"enabled": true, "host": "smtp.mailgun.org", "port": 587, "encryption": "starttls", "sender_email": "noreply@myshop.com"}'
import requests
resp = requests.patch(
"https://yourshop.zeroshop.io/admin/api/smtp",
headers={"Authorization": "Bearer zspat_..."},
json={
"enabled": True,
"host": "smtp.mailgun.org",
"port": 587,
"encryption": "starttls",
"sender_email": "noreply@myshop.com"
}
)
assert resp.status_code == 204
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/smtp",
{
method: "PATCH",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
enabled: true,
host: "smtp.mailgun.org",
port: 587,
encryption: "starttls",
sender_email: "noreply@myshop.com"
})
}
);
// 204 No Content
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/smtp")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { enabled: true, host: "smtp.mailgun.org", port: 587 }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
# 204 No Content
/admin/api/smtp/test
Send a test email to the shop's contact email address using the saved SMTP settings. SMTP must be enabled and fully configured.
{
"ok": true,
"message": "Test email sent to hello@myshop.com"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/smtp/test" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/smtp/test",
headers={"Authorization": "Bearer zspat_..."}
)
result = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/smtp/test",
{
method: "POST",
headers: { "Authorization": "Bearer zspat_..." }
}
);
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/smtp/test")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/integrations
List all configured integrations (payment gateways, marketing tools, shipping providers), ordered by category then provider.
[
{
"id": 1,
"provider": "stripe-connect",
"category": "payments",
"enabled": true,
"sandbox": false,
"config": "{\"account_id\":\"acct_1234\",\"charges_enabled\":true,\"details_submitted\":true}",
"created_at": "2025-10-01T10:00:00Z",
"updated_at": "2025-11-15T09:00:00Z"
},
{
"id": 2,
"provider": "manual",
"category": "payments",
"enabled": true,
"sandbox": false,
"config": "{}",
"created_at": "2025-10-01T10:00:00Z",
"updated_at": "2025-10-01T10:00:00Z"
}
]
curl "https://yourshop.zeroshop.io/admin/api/integrations" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/integrations",
headers={"Authorization": "Bearer zspat_..."}
)
integrations = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/integrations",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const integrations = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/integrations")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/integrations/{provider}
Get a single integration by provider name.
| Name | Type | Required | Description |
|---|---|---|---|
| provider | string | required | Provider name (e.g. "stripe-connect", "easyship", "manual") |
{
"id": 1,
"provider": "stripe-connect",
"category": "payments",
"enabled": true,
"sandbox": false,
"config": "{\"account_id\":\"acct_1234\",\"charges_enabled\":true,\"details_submitted\":true}",
"created_at": "2025-10-01T10:00:00Z",
"updated_at": "2025-11-15T09:00:00Z"
}
curl "https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect",
headers={"Authorization": "Bearer zspat_..."}
)
integration = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const integration = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/integrations/{provider}
Create or update an integration. Cannot disable the last active payment method.
| Name | Type | Required | Description |
|---|---|---|---|
| provider | string | required | Provider name (e.g. "easyship", "manual") |
{
"category": "payments",
"enabled": true,
"sandbox": false,
"config": {
"api_key": "sk_live_..."
}
}
{
"id": 1,
"provider": "easyship",
"category": "shipping",
"enabled": true,
"sandbox": false,
"config": "{\"api_key\":\"sk_live_...\"}",
"created_at": "2025-10-01T10:00:00Z",
"updated_at": "2025-11-20T14:00:00Z"
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/integrations/easyship" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"category": "shipping", "enabled": true, "sandbox": false, "config": {"api_key": "sk_live_..."}}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/integrations/easyship",
headers={"Authorization": "Bearer zspat_..."},
json={
"category": "shipping",
"enabled": True,
"sandbox": False,
"config": {"api_key": "sk_live_..."}
}
)
integration = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/integrations/easyship",
{
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
category: "shipping",
enabled: true,
sandbox: false,
config: { api_key: "sk_live_..." }
})
}
);
const integration = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/integrations/easyship")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { category: "shipping", enabled: true, sandbox: false, config: { api_key: "sk_live_..." } }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/integrations/stripe-connect/create-account
Create a Stripe Express Connected Account for this shop. Only one account can be connected at a time.
{
"id": 1,
"provider": "stripe-connect",
"category": "payments",
"enabled": true,
"sandbox": true,
"config": "{\"account_id\":\"acct_1N7abc\",\"charges_enabled\":false,\"details_submitted\":false}",
"created_at": "2025-11-20T14:00:00Z",
"updated_at": "2025-11-20T14:00:00Z"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/create-account" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/create-account",
headers={"Authorization": "Bearer zspat_..."}
)
result = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/create-account",
{
method: "POST",
headers: { "Authorization": "Bearer zspat_..." }
}
);
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/create-account")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/integrations/stripe-connect/onboarding-link
Generate a Stripe Account Link for merchant onboarding. The merchant is redirected to Stripe to enter business details and bank info.
{
"return_url": "https://yourshop.zeroshop.io/admin/settings/integrations",
"refresh_url": "https://yourshop.zeroshop.io/admin/settings/integrations"
}
{
"url": "https://connect.stripe.com/setup/s/acct_1N7abc/abc123"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/onboarding-link" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"return_url": "https://yourshop.zeroshop.io/admin/settings", "refresh_url": "https://yourshop.zeroshop.io/admin/settings"}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/onboarding-link",
headers={"Authorization": "Bearer zspat_..."},
json={
"return_url": "https://yourshop.zeroshop.io/admin/settings",
"refresh_url": "https://yourshop.zeroshop.io/admin/settings"
}
)
data = resp.json()
# Redirect merchant to data["url"]
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/onboarding-link",
{
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
return_url: "https://yourshop.zeroshop.io/admin/settings",
refresh_url: "https://yourshop.zeroshop.io/admin/settings"
})
}
);
const { url } = await resp.json();
window.location.href = url;
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/onboarding-link")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { return_url: "https://yourshop.zeroshop.io/admin/settings", refresh_url: "https://yourshop.zeroshop.io/admin/settings" }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/integrations/stripe-connect/status
Refresh the connected Stripe account's status by calling the Stripe API. Updates cached charges_enabled and details_submitted flags.
{
"id": 1,
"provider": "stripe-connect",
"category": "payments",
"enabled": true,
"sandbox": true,
"config": "{\"account_id\":\"acct_1N7abc\",\"charges_enabled\":true,\"details_submitted\":true}",
"created_at": "2025-11-20T14:00:00Z",
"updated_at": "2025-11-21T10:00:00Z"
}
curl "https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/status" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/status",
headers={"Authorization": "Bearer zspat_..."}
)
status = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/status",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const status = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/status")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/integrations/stripe-connect/disconnect
Remove the connected Stripe account. The account is not deleted on Stripe's side. Cannot disconnect if it is the last active payment method.
{
"id": 1,
"provider": "stripe-connect",
"category": "payments",
"enabled": false,
"sandbox": true,
"config": "{}",
"created_at": "2025-11-20T14:00:00Z",
"updated_at": "2025-11-22T09:00:00Z"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/disconnect" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/disconnect",
headers={"Authorization": "Bearer zspat_..."}
)
result = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/disconnect",
{
method: "POST",
headers: { "Authorization": "Bearer zspat_..." }
}
);
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/disconnect")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/integrations/easyship/test
Test the Easyship API connection by sending a minimal rates request with the configured API key and origin address.
{
"ok": true,
"message": "Connection successful."
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/integrations/easyship/test" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/integrations/easyship/test",
headers={"Authorization": "Bearer zspat_..."}
)
result = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/integrations/easyship/test",
{
method: "POST",
headers: { "Authorization": "Bearer zspat_..." }
}
);
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/integrations/easyship/test")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/marketing/channels
Returns first-touch and last-touch revenue/order counts grouped by UTM source, UTM campaign, and HTTP referrer. Used for the order-attribution report. Test orders are excluded.
| Name | Type | Required | Description |
|---|---|---|---|
| from | string | required | Start of the date range, inclusive. ISO 8601 YYYY-MM-DD. |
| to | string | required | End of the date range, inclusive. ISO 8601 YYYY-MM-DD. |
{
"first_touch": {
"by_source": [
{
"key": "google",
"orders": 42,
"revenue_cents": 158400
},
{
"key": "newsletter",
"orders": 11,
"revenue_cents": 39900
},
{
"key": null,
"orders": 7,
"revenue_cents": 21000
}
],
"by_campaign": [
{
"key": "spring-sale",
"orders": 18,
"revenue_cents": 72000
}
],
"by_referrer": [
{
"key": "https://www.google.com/",
"orders": 30,
"revenue_cents": 110000
}
]
},
"last_touch": {
"by_source": [
{
"key": "google",
"orders": 51,
"revenue_cents": 189500
},
{
"key": null,
"orders": 6,
"revenue_cents": 18000
}
],
"by_campaign": [
{
"key": "spring-sale",
"orders": 22,
"revenue_cents": 87600
}
],
"by_referrer": [
{
"key": "https://www.google.com/",
"orders": 35,
"revenue_cents": 128000
}
]
}
}
curl "https://yourshop.zeroshop.io/admin/api/marketing/channels?from=2026-01-01&to=2026-01-31" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/marketing/channels",
headers={"Authorization": "Bearer zspat_..."},
params={"from": "2026-01-01", "to": "2026-01-31"}
)
report = resp.json()
const params = new URLSearchParams({ from: "2026-01-01", to: "2026-01-31" });
const resp = await fetch(
`https://yourshop.zeroshop.io/admin/api/marketing/channels?${params}`,
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const report = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/marketing/channels")
uri.query = URI.encode_www_form(from: "2026-01-01", to: "2026-01-31")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
report = JSON.parse(resp.body)
/admin/api/seo/static-pages
List SEO meta for all eight built-in storefront routes.
{
"data": [
{
"route": "home",
"meta_title": "Welcome to Acme",
"meta_description": "Hand-curated goods, shipped worldwide.",
"og_image_media_id": 14,
"og_image_url": "https://cdn.zeroshop.io/acme/og-home.webp",
"updated_at": "2026-04-30T18:22:00Z"
},
{
"route": "cart",
"meta_title": "",
"meta_description": "",
"og_image_media_id": null,
"og_image_url": null,
"updated_at": "2026-04-30T18:22:00Z"
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/seo/static-pages" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/seo/static-pages",
headers={"Authorization": "Bearer zspat_..."}
)
pages = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/seo/static-pages", {
headers: { "Authorization": "Bearer zspat_..." }
});
const pages = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/seo/static-pages")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
pages = JSON.parse(resp.body)
/admin/api/seo/static-pages/{route}
Fetch SEO meta for a single built-in route.
| Name | Type | Required | Description |
|---|---|---|---|
| route | string | required | One of: "home", "cart", "checkout", "search", "account", "login", "register", "not_found". |
{
"route": "home",
"meta_title": "Welcome to Acme",
"meta_description": "Hand-curated goods, shipped worldwide.",
"og_image_media_id": 14,
"og_image_url": "https://cdn.zeroshop.io/acme/og-home.webp",
"updated_at": "2026-04-30T18:22:00Z"
}
curl "https://yourshop.zeroshop.io/admin/api/seo/static-pages/home" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/seo/static-pages/home",
headers={"Authorization": "Bearer zspat_..."}
)
page = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/seo/static-pages/home", {
headers: { "Authorization": "Bearer zspat_..." }
});
const page = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/seo/static-pages/home")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
page = JSON.parse(resp.body)
/admin/api/seo/static-pages/{route}
Set SEO meta for a built-in route. Empty strings fall back to template defaults; pass `og_image_media_id: null` to clear.
{
"meta_title": "Welcome to Acme",
"meta_description": "Hand-curated goods, shipped worldwide.",
"og_image_media_id": 14
}
{
"route": "home",
"meta_title": "Welcome to Acme",
"meta_description": "Hand-curated goods, shipped worldwide.",
"og_image_media_id": 14,
"og_image_url": "https://cdn.zeroshop.io/acme/og-home.webp",
"updated_at": "2026-04-30T18:25:00Z"
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/seo/static-pages/home" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"meta_title":"Welcome to Acme","meta_description":"Hand-curated goods.","og_image_media_id":14}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/seo/static-pages/home",
json={
"meta_title": "Welcome to Acme",
"meta_description": "Hand-curated goods.",
"og_image_media_id": 14
},
headers={"Authorization": "Bearer zspat_..."}
)
page = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/seo/static-pages/home", {
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
meta_title: "Welcome to Acme",
meta_description: "Hand-curated goods.",
og_image_media_id: 14
})
});
const page = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/seo/static-pages/home")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = {
meta_title: "Welcome to Acme",
meta_description: "Hand-curated goods.",
og_image_media_id: 14
}.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
page = JSON.parse(resp.body)
/admin/api/seo/static-pages/{route}/translations
List per-locale overrides for a static page's meta_title and meta_description.
{
"data": [
{
"locale": "de",
"meta_title": "Willkommen bei Acme",
"meta_description": "Handverlesene Produkte, weltweiter Versand."
},
{
"locale": "fr",
"meta_title": "Bienvenue chez Acme",
"meta_description": "Produits triés sur le volet, expédiés dans le monde entier."
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/seo/static-pages/home/translations" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/seo/static-pages/home/translations",
headers={"Authorization": "Bearer zspat_..."}
)
translations = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/seo/static-pages/home/translations",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const translations = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/seo/static-pages/home/translations")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/seo/static-pages/{route}/translations
Set per-locale overrides for meta_title and meta_description. Replaces all overrides for the route.
{
"translations": [
{
"locale": "de",
"meta_title": "Willkommen bei Acme",
"meta_description": "Handverlesene Produkte, weltweiter Versand."
},
{
"locale": "fr",
"meta_title": "Bienvenue chez Acme",
"meta_description": "Produits triés sur le volet."
}
]
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/seo/static-pages/home/translations" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"translations":[{"locale":"de","meta_title":"Willkommen","meta_description":"…"}]}'
import requests
requests.put(
"https://yourshop.zeroshop.io/admin/api/seo/static-pages/home/translations",
json={"translations": [
{"locale": "de", "meta_title": "Willkommen", "meta_description": "…"}
]},
headers={"Authorization": "Bearer zspat_..."}
)
await fetch("https://yourshop.zeroshop.io/admin/api/seo/static-pages/home/translations", {
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
translations: [
{ locale: "de", meta_title: "Willkommen", meta_description: "…" }
]
})
});
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/seo/static-pages/home/translations")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = {
translations: [
{ locale: "de", meta_title: "Willkommen", meta_description: "…" }
]
}.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/custom-domain/status
Returns the current custom-domain provisioning status as a tagged object keyed by `state` (one of none, pending_dns, pending_ssl, active, failed). For any non-none state the payload includes the CNAME records to add at the registrar and the raw Cloudflare ssl_status. Same shape and permission as GET /admin/api/custom-domain; exists so the setup wizard can poll while the domain is in a non-terminal state.
{
"state": "pending_ssl",
"hostname": "shop.acme.com",
"primary_record": {
"kind": "CNAME",
"name": "shop.acme.com",
"value": "acme.zeroshop.io"
},
"dcv_record": {
"kind": "CNAME",
"name": "_acme-challenge.shop.acme.com",
"value": "shop.acme.com.abcdef.dcv.cloudflare.com"
},
"ssl_status": "pending_validation",
"last_error": null,
"last_synced_at": "2026-05-29T10:00:00Z"
}
curl "https://yourshop.zeroshop.io/admin/api/custom-domain/status" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/custom-domain/status",
headers={"Authorization": "Bearer zspat_..."}
)
status = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/custom-domain/status",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const status = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/custom-domain/status")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/settings/favicon
Upload the shop favicon. Multipart/form-data with a single `file` field (PNG or JPEG, source ≤ 2 MB). Generates 32px and 180px PNGs plus stores the source, then returns cache-busted public URLs. Tier-gated by per-file size only.
{
"file": "<binary PNG or JPEG>"
}
{
"favicon_url": "https://cdn.zeroshop.io/acme/favicon/180.png?v=1717063200",
"icon_url": "https://cdn.zeroshop.io/acme/favicon/32.png?v=1717063200",
"apple_url": "https://cdn.zeroshop.io/acme/favicon/180.png?v=1717063200",
"has_favicon": true
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/settings/favicon" \
-H "Authorization: Bearer zspat_..." \
-F "file=@favicon.png"
import requests
with open("favicon.png", "rb") as f:
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/settings/favicon",
headers={"Authorization": "Bearer zspat_..."},
files={"file": ("favicon.png", f, "image/png")}
)
result = resp.json()
const form = new FormData();
form.append("file", fileInput.files[0]);
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/settings/favicon",
{
method: "POST",
headers: { "Authorization": "Bearer zspat_..." },
body: form
}
);
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/settings/favicon")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.set_form([["file", File.open("favicon.png"), { filename: "favicon.png", content_type: "image/png" }]], "multipart/form-data")
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/settings/checkout-logo
Upload the checkout-page logo. Multipart/form-data with a single `file` field (PNG or JPEG, source ≤ 2 MB). The image is downscaled to a PNG, stored, and the cache-busted public URL returned. Tier-gated by per-file size only.
{
"file": "<binary PNG or JPEG>"
}
{
"checkout_logo_url": "https://cdn.zeroshop.io/acme/checkout-logo.png?v=1717063200",
"has_checkout_logo": true
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/settings/checkout-logo" \
-H "Authorization: Bearer zspat_..." \
-F "file=@logo.png"
import requests
with open("logo.png", "rb") as f:
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/settings/checkout-logo",
headers={"Authorization": "Bearer zspat_..."},
files={"file": ("logo.png", f, "image/png")}
)
result = resp.json()
const form = new FormData();
form.append("file", fileInput.files[0]);
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/settings/checkout-logo",
{
method: "POST",
headers: { "Authorization": "Bearer zspat_..." },
body: form
}
);
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/settings/checkout-logo")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.set_form([["file", File.open("logo.png"), { filename: "logo.png", content_type: "image/png" }]], "multipart/form-data")
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/integrations/stripe-connect/dashboard-link
Create a one-time Stripe Express Dashboard login link for the connected account. Redirect the merchant to the returned URL to view payouts and balances on Stripe's hosted dashboard. The link is single-use and short-lived; request it on demand.
{
"url": "https://connect.stripe.com/express/acct_1N7abc/abc123login"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/dashboard-link" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/dashboard-link",
headers={"Authorization": "Bearer zspat_..."}
)
data = resp.json()
# Redirect merchant to data["url"]
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/dashboard-link",
{
method: "POST",
headers: { "Authorization": "Bearer zspat_..." }
}
);
const { url } = await resp.json();
window.location.href = url;
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/integrations/stripe-connect/dashboard-link")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/apple-pay/status
Returns the cached Apple Pay domain-registration status from the registry without calling Stripe. Cheap enough to call on every Payments page render. status is one of active, inactive, pending, or null when no registration exists; status_reason carries Stripe's reason when not active.
{
"domain": "shop.acme.com",
"status": "pending",
"status_reason": "missing_payment_method_domain_file"
}
curl "https://yourshop.zeroshop.io/admin/api/apple-pay/status" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/apple-pay/status",
headers={"Authorization": "Bearer zspat_..."}
)
status = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/apple-pay/status",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const status = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/apple-pay/status")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/apple-pay/recheck
Force a fresh Stripe round-trip to re-register and re-check the Apple Pay domain under the per-tenant reconcile lock, then return the refreshed cached row. Powers the SPA's "Re-check" button. Errors from the underlying reconcile are logged and swallowed -- the response always returns whatever the registry currently holds, so the same payload shape as GET status is returned even if Stripe is momentarily down. Takes no body.
{
"domain": "shop.acme.com",
"status": "active",
"status_reason": null
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/apple-pay/recheck" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/apple-pay/recheck",
headers={"Authorization": "Bearer zspat_..."}
)
status = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/apple-pay/recheck",
{
method: "POST",
headers: { "Authorization": "Bearer zspat_..." }
}
);
const status = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/apple-pay/recheck")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
We value your privacy
We use cookies for essential site functionality and, with your consent, analytics to understand how our platform is used. No personal data is shared with third parties. See our Privacy Policy for details.