Skip to main content

Documentation Index

Fetch the complete documentation index at: https://appstleinc-aeca3e0a.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Build a seamless, zero-configuration integration between your app and Appstle Memberships. Once connected, your app gets a scoped API token for each merchant — no manual key exchange needed.
Why become a partner?
  • Frictionless merchant onboarding — one-click connect from either dashboard
  • No API paywall — merchants don’t need a paid API plan to use your integration
  • Scoped tokens — each merchant gets an isolated API key; revocable at any time
  • Automatic cleanup — when a merchant disconnects or uninstalls, access is revoked instantly

How it works

The Partner Integration Framework uses a secure handshake protocol. Either side — your app or Appstle — can initiate the connection. Both flows end with your app receiving a scoped API token.
Merchant approval: When your app initiates a connection (Flow A), the merchant must approve it from their Appstle dashboard before you receive an API token. When the merchant initiates from Appstle’s side (Flow B), the connection is approved instantly because the merchant is the one clicking “Connect.”

Getting started

Step 1: Get onboarded

To get started, reach out to the Appstle team at support@appstle.com with the information below. Our team will set up your partner account and send you your credentials.

What you’ll need to provide

#FieldRequired?DescriptionExample
1App / Company NameYesYour app or company name. Displayed to merchants when they browse available partner integrations in the Appstle dashboard.SearchPie
2Partner IDYesA unique, lowercase slug that identifies your app in API URLs. Use only lowercase letters and hyphens. Once set, this cannot be changed.search-pie
3Base URLYesThe HTTPS base URL where Appstle will send callback requests (connect, verify, approved). Must be publicly accessible — Appstle will not call HTTP or localhost URLs.https://api.searchpie.com
4Contact EmailYesThe email address where we’ll send your Partner Secret and any onboarding follow-ups. Use a team email if possible — the secret is shown only once.dev-team@searchpie.com
5Authentication ModeOptionalHow your API calls are authenticated. Choose one: Partner Secret (simpler — pass secret in a header) or HMAC-SHA256 (more secure — sign each request). Defaults to Partner Secret if not specified. See Step 1b for details.Partner Secret
6Connect ModeOptionalHow merchant connections are established. Choose one: Nonce Handshake (full two-way verification — you receive an Appstle API key) or Simple Token Exchange (streamlined — you provide your own token for Appstle to call your API). Defaults to Nonce Handshake if not specified. See Step 1c for details.Nonce Handshake
7Custom Endpoint PathsOptionalBy default, Appstle calls /appstle/connect and /appstle/verify on your Base URL. If you need different paths (e.g., /webhooks/appstle/connect), specify them here./webhooks/appstle/connect
8Sync PathOptionalIf you want Appstle to push membership data to your app (e.g., when memberships are created, cancelled, or plans change), provide the path on your server where Appstle should send these payloads. See Data Sync for details./appstle/sync
9App LogoOptionalA square logo (PNG or SVG, at least 128×128px) displayed next to your app name in the merchant’s Appstle dashboard. If not provided, a placeholder icon is used.
Not sure about some of these? Only the first four fields are required to get started. You can always reach out to support@appstle.com to change your authentication mode, connect mode, or add a sync path later.

What you’ll receive

Once onboarded, you’ll receive three values:
CredentialExampleDescription
Partner IDsearch-pieYour unique identifier, as requested. Becomes part of the API URL.
Partner SecretxK9mQ2vL... (48 chars)A secret key used to authenticate your API calls. Treat this like a password.
Base URLhttps://membership-admin.appstle.comAppstle’s API base URL. Same for all partners.
Your Partner Secret is shown only once during onboarding. Copy it immediately and store it in a secure location (environment variable, secrets manager, etc.). If you lose it, contact Appstle to rotate it — the old secret will be invalidated immediately.
Store your credentials as environment variables:
# .env (never commit this file)
APPSTLE_PARTNER_ID=search-pie
APPSTLE_PARTNER_SECRET=xK9mQ2vLa8nR3pY...       # if using Partner Secret auth
APPSTLE_HMAC_KEY=your-hmac-key-here               # if using HMAC-SHA256 auth
APPSTLE_BASE_URL=https://membership-admin.appstle.com

Step 1b: Choose your authentication mode

Appstle supports two ways to authenticate partner API calls. Your auth mode is configured during onboarding.

Option A: Partner Secret (default)

The simplest approach. Pass your secret in a header with every request:
X-Partner-Secret: your-partner-secret
That’s it. No computation needed. Good for getting started quickly.

Option B: HMAC-SHA256

A more secure approach where requests are signed with a shared HMAC key. Instead of sending the secret directly, you compute a signature over the request body. Headers required:
X-Partner-Timestamp: 1709856000
X-Partner-Signature: 5a3c1f2e9b8d7a6c...
How to compute the signature:
  1. Get the current Unix timestamp (seconds, not milliseconds)
  2. Concatenate the timestamp and the raw JSON request body: timestamp + body
  3. Compute HMAC-SHA256 of that string using your HMAC key
  4. Send the hex-encoded result in X-Partner-Signature
Timestamp validation: Appstle rejects requests where the timestamp is more than 5 minutes from the server’s current time. Make sure your server clock is synced (NTP).
const crypto = require('crypto');

function signRequest(body, hmacKey) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const data = timestamp + body;
  const signature = crypto
    .createHmac('sha256', hmacKey)
    .update(data)
    .digest('hex');

  return {
    'X-Partner-Timestamp': timestamp,
    'X-Partner-Signature': signature,
    'Content-Type': 'application/json',
  };
}

// Usage
const body = JSON.stringify({ shop_domain: 'cool-store.myshopify.com' });
const headers = signRequest(body, process.env.APPSTLE_HMAC_KEY);
Which should I choose?
  • Partner Secret — simpler to implement, fine for most integrations
  • HMAC-SHA256 — better security (secret never sent over the wire), recommended for high-volume or security-sensitive integrations
Both are equally supported. You can switch modes later by contacting Appstle.

Step 1c: Choose your connect mode

Appstle supports two ways to establish merchant connections. Your connect mode is configured during onboarding.

Option A: Nonce Handshake (default)

The full two-way verification flow described in this guide. Both sides verify each other using a one-time nonce. After the handshake, your app receives an Appstle API key (apst_...) to call Appstle’s External API. Best for: partners who want to read/write data in Appstle (membership contracts, plans, billing, etc.)

Option B: Simple Token Exchange

A streamlined flow where your app sends its own access token to Appstle (or Appstle calls your connect endpoint and you return one). No nonce, no verify endpoint needed. Appstle stores your token and uses it to call your API when needed. Best for: partners where Appstle needs to call the partner’s API (e.g., syncing data to the partner’s platform), rather than the partner calling Appstle’s API. Key difference from Nonce Handshake: in Simple Token Exchange, your app provides its own access token to Appstle. Appstle stores this token and uses it to push data to your API (via your sync_path — see Data Sync below). Your app does not receive an Appstle API key in this mode.
Need both directions? If you need to both push data to Appstle AND have Appstle push data to you, use the Nonce Handshake mode and provide a sync_path during onboarding. Contact support@appstle.com to discuss your use case.
How Simple Token Exchange works. Partner-initiated:
curl -X POST "https://membership-admin.appstle.com/api/partner/your-partner-id/connect" \
  -H "X-Partner-Timestamp: 1709856000" \
  -H "X-Partner-Signature: 5a3c1f2e..." \
  -H "Content-Type: application/json" \
  -d '{
    "shop_domain": "cool-store.myshopify.com",
    "access_token": "your-apps-token-for-this-merchant"
  }'
Response:
{
  "status": "pending_merchant_approval"
}
Your access_token is stored securely but will not be activated until the merchant approves the connection from their Appstle dashboard. Once approved, Appstle calls your /appstle/approved endpoint to confirm (see Handling the approval callback).
Appstle-initiated: Appstle calls your /appstle/connect endpoint with { "shop_domain": "..." }. Your app responds with:
{
  "success": true,
  "access_token": "your-apps-token-for-this-merchant"
}
With Simple Token Exchange, your app does NOT receive an Appstle API key. If you also need to call Appstle’s External API, use the Nonce Handshake mode instead.

Step 2: Understand the callback nonce

This section applies to Nonce Handshake mode only. If you’re using Simple Token Exchange, skip to Step 4.
Before implementing, you need to understand the callback nonce — it’s the core security mechanism of the handshake.

What is a callback nonce?

A nonce (number used once) is a random, single-use string that proves both sides of the connection are who they claim to be. It prevents replay attacks and ensures the handshake can’t be forged.

Requirements

RequirementDetail
LengthAt least 32 bytes (64 hex characters)
RandomnessMust be cryptographically random — do NOT use Math.random(), rand(), timestamps, or UUIDs
Single-useEach nonce must be used exactly once, then deleted
ExpiryNonces expire after 5 minutes on Appstle’s side. Your storage should also expire them.
StorageStore temporarily with a TTL. Redis, DynamoDB, or any key-value store with expiry works. Database with a cleanup job is also fine.

How to generate a nonce

Use your language’s cryptographically secure random number generator.
const crypto = require('crypto');

// Generate a 32-byte (64 hex character) cryptographically random nonce
const nonce = crypto.randomBytes(32).toString('hex');
// Result: "a1b2c3d4e5f6...64 characters total"
Common mistakes:
  • Math.random().toString(36) — not cryptographically random, predictable
  • uuid.v4() — UUIDs are not designed as security tokens (some implementations use weak RNG)
  • Date.now().toString() — trivially guessable
  • Reusing nonces across multiple connect attempts
Always use your language’s crypto / secrets / SecureRandom module.

How to store a nonce

Store the nonce temporarily, keyed by shop domain, with a 5-minute expiry. Delete it after verification.
const Redis = require('ioredis');
const redis = new Redis();

// Store nonce with 5-minute TTL
async function storeNonce(shopDomain, nonce) {
  const key = `appstle:nonce:${shopDomain}`;
  await redis.set(key, nonce, 'EX', 300); // 300 seconds = 5 minutes
}

// Retrieve and delete nonce (single atomic operation)
async function verifyAndDeleteNonce(shopDomain, nonceToCheck) {
  const key = `appstle:nonce:${shopDomain}`;
  const storedNonce = await redis.get(key);

  if (!storedNonce || storedNonce !== nonceToCheck) {
    return false;
  }

  await redis.del(key);
  return true;
}

Step 3: Implement your endpoints

Your app must expose two HTTP endpoints that Appstle calls during the connection handshake. The paths default to /appstle/connect and /appstle/verify but can be customized during onboarding. Both endpoints must:
  • Accept POST requests with a JSON body
  • Return JSON responses
  • Be accessible over HTTPS (Appstle will not call HTTP endpoints)
  • Respond within 10 seconds (or the request will time out)

Endpoint 1: POST /appstle/connect

When is this called? Appstle calls this when a merchant initiates the connection from Appstle’s dashboard (Flow B). What does it receive?
{
  "shop_domain": "cool-store.myshopify.com",
  "app": "memberships",
  "callback_url": "https://membership-admin.appstle.com/api/partner/your-partner-id/verify",
  "callback_nonce": "7f3a9b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a"
}
FieldTypeDescription
shop_domainstringThe merchant’s Shopify domain (e.g. cool-store.myshopify.com)
appstringAlways "memberships" — identifies which Appstle app is connecting
callback_urlstringThe exact URL your app must call to complete the handshake
callback_noncestringA one-time-use token generated by Appstle. Expires in 5 minutes.
What should your app do?
  1. Validate the shop — check that this shop_domain exists in your system. If you don’t recognize the shop, return an error.
  2. Store the nonce and callback URL — save callback_nonce and callback_url associated with this shop_domain. You’ll need them to complete the handshake.
  3. Call back to Appstle — either immediately (auto-approve) or after merchant confirmation, call the callback_url to complete the connection. See Completing the handshake below.
  4. Return a success response — any 2xx status code tells Appstle the request was received.
Node.js (Express)
const express = require('express');
const axios = require('axios');
const router = express.Router();

router.post('/appstle/connect', async (req, res) => {
  const { shop_domain, app, callback_url, callback_nonce } = req.body;

  // 1. Validate: does this shop exist in your system?
  const shop = await db.shops.findOne({ domain: shop_domain });
  if (!shop) {
    return res.status(400).json({ error: 'Shop not found in our system' });
  }

  // 2. Store the nonce and callback URL for this shop
  await db.pendingConnections.upsert({
    shopDomain: shop_domain,
    callbackUrl: callback_url,
    callbackNonce: callback_nonce,
    createdAt: new Date(),
    expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5 minutes
  });

  // 3. Option A: Auto-approve (call back immediately)
  try {
    const response = await axios.post(callback_url, {
      shop_domain: shop_domain,
      callback_nonce: callback_nonce,
    }, {
      headers: {
        'X-Partner-Secret': process.env.APPSTLE_PARTNER_SECRET,
        'Content-Type': 'application/json',
      },
    });

    if (response.data.verified && response.data.access_token) {
      // 4. Store the access token for this merchant
      await db.appstleTokens.upsert({
        shopDomain: shop_domain,
        accessToken: response.data.access_token,
        connectedAt: new Date(),
      });
    }
  } catch (err) {
    console.error('Failed to complete Appstle handshake:', err.message);
  }

  // 5. Return success to Appstle
  res.json({ success: true });
});

Endpoint 2: POST /appstle/verify

When is this called? Appstle calls this when a merchant initiates the connection from your app’s dashboard (Flow A). Appstle is asking your app: “Did you actually send this nonce?” What does it receive?
{
  "shop_domain": "cool-store.myshopify.com",
  "callback_nonce": "a1b2c3d4e5f6...the-nonce-you-generated"
}
FieldTypeDescription
shop_domainstringThe merchant’s Shopify domain
callback_noncestringThe nonce your app originally sent in the /connect call
What should your app do?
  1. Look up the stored nonce for this shop_domain
  2. Compare the callback_nonce from the request against your stored nonce
  3. If they match: delete the stored nonce (it’s single-use) and return { "verified": true }
  4. If they don’t match: return { "verified": false }
Node.js (Express)
router.post('/appstle/verify', async (req, res) => {
  const { shop_domain, callback_nonce } = req.body;

  // 1. Look up the stored nonce for this shop
  const isValid = await verifyAndDeleteNonce(shop_domain, callback_nonce);

  // 2. Return the result
  res.json({ verified: isValid });
});

Step 4: Implement the connect flow (your dashboard)

Now build the merchant-facing “Connect Appstle Memberships” button in your app’s dashboard.

Partner-initiated connect (Flow A) — step by step

This is the flow where the merchant clicks “Connect Appstle” in your dashboard.
1

Merchant clicks "Connect Appstle" in your dashboard

The merchant initiates the connection from inside your app’s UI.
2

Your app generates a cryptographically random nonce

Store it keyed by shop_domain with a 5-minute TTL.
3

Your app calls POST /api/partner/{id}/connect

Send the shop_domain, the callback_nonce, and your Partner Secret.
4

Appstle validates your Partner Secret

Appstle also confirms the shop has Appstle Memberships installed.
5

Appstle calls YOUR /appstle/verify endpoint

Payload: the shop_domain and the same callback_nonce.
6

Your /appstle/verify checks the nonce

Confirm it matches, delete it, return { "verified": true }.
7

Appstle returns pending_merchant_approval

The connection is pending — no access token has been issued yet.
8

Merchant approves in their Appstle dashboard

They open Settings → Partner Connections and click Approve.
9

Appstle creates a scoped API key

The token is POSTed to YOUR /appstle/approved endpoint.
10

Your app stores the access_token

Show “Connected!” to the merchant. You’re done.
Full implementation (Node.js):
const crypto = require('crypto');
const axios = require('axios');

const PARTNER_ID = process.env.APPSTLE_PARTNER_ID;
const PARTNER_SECRET = process.env.APPSTLE_PARTNER_SECRET;
const APPSTLE_BASE = process.env.APPSTLE_BASE_URL; // https://membership-admin.appstle.com

// Called when merchant clicks "Connect Appstle" in your dashboard
async function connectToAppstle(shopDomain) {

  // Step 2: Generate a cryptographically random nonce
  const nonce = crypto.randomBytes(32).toString('hex');

  // Store it so your /appstle/verify endpoint can look it up later
  await storeNonce(shopDomain, nonce); // see nonce storage examples above

  // Step 3: Call Appstle's partner connect endpoint
  const response = await axios.post(
    `${APPSTLE_BASE}/api/partner/${PARTNER_ID}/connect`,
    {
      shop_domain: shopDomain,
      callback_nonce: nonce,
    },
    {
      headers: {
        'X-Partner-Secret': PARTNER_SECRET,
        'Content-Type': 'application/json',
      },
    }
  );

  // Steps 4-6 happen automatically (Appstle calls your /appstle/verify)

  // Step 7: Appstle returns pending status — token is NOT delivered yet
  const { status } = response.data;

  if (status === 'pending_merchant_approval') {
    // The merchant needs to approve in their Appstle dashboard.
    // Once approved, Appstle will POST the token to your /appstle/approved endpoint.
    await markConnectionPending(shopDomain);
    return { pending: true };
  }

  throw new Error('Connection failed');
}
curl equivalent:
curl -X POST "https://membership-admin.appstle.com/api/partner/search-pie/connect" \
  -H "X-Partner-Secret: xK9mQ2vLa8nR3pY..." \
  -H "Content-Type: application/json" \
  -d '{
    "shop_domain": "cool-store.myshopify.com",
    "callback_nonce": "a1b2c3d4e5f67890abcdef1234567890a1b2c3d4e5f67890abcdef1234567890"
  }'
Success response:
{
  "status": "pending_merchant_approval"
}
What happens next? The merchant will see a “Pending Request” in their Appstle dashboard under Settings → Partner Connections. (This menu appears automatically once a partner initiates a connection request — it is not visible before any partner has connected.) When they click “Approve,” Appstle creates a scoped API key and delivers it to your /appstle/approved endpoint (see Handling the approval callback below). Pending requests expire after 30 days if not acted on.
Deep link to approval screen: You can redirect the merchant directly to the approval screen to minimize friction:
https://admin.shopify.com/store/{shop-handle}/apps/appstle-memberships/settings/partner-connections
Replace {shop-handle} with the merchant’s store handle (the part before .myshopify.com). This takes them straight to the pending connection for one-click approval. You can trigger this redirect in your UI immediately after receiving the pending_merchant_approval response.

Appstle-initiated connect (Flow B) — step by step

This is the flow where the merchant clicks “Connect” in Appstle’s dashboard.
1

Merchant clicks "Connect {YourApp}" in Appstle's dashboard

The connection is initiated from inside Appstle, not your UI.
2

Appstle calls POST /api/partner/{id}/initiate-connect

Internal Appstle call — your app is not involved yet.
3

Appstle generates a nonce (5-min TTL) and calls YOUR /appstle/connect

Payload: shop_domain, app, callback_url, and callback_nonce.
4

Your /appstle/connect stores the nonce and callback_url

Persist them keyed by shop_domain for the verify step.
5

Your app calls the callback_url (Appstle's /verify endpoint)

Send shop_domain, callback_nonce, and your Partner Secret.
6

Appstle verifies the nonce and issues a scoped API key

The access_token is returned in the response body.
7

Your app stores the access_token

Connection complete.
Completing the handshake (Flow B)
After your /appstle/connect endpoint receives the nonce and callback URL, your app completes the connection by calling Appstle’s verify endpoint:
curl -X POST "https://membership-admin.appstle.com/api/partner/search-pie/verify" \
  -H "X-Partner-Secret: xK9mQ2vLa8nR3pY..." \
  -H "Content-Type: application/json" \
  -d '{
    "shop_domain": "cool-store.myshopify.com",
    "callback_nonce": "the-nonce-appstle-sent-in-the-connect-call"
  }'
Success response:
{
  "verified": true,
  "access_token": "apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012"
}
Failed response (nonce expired or mismatched):
{
  "verified": false
}
You must call the verify endpoint within 5 minutes of receiving the nonce. After that, the nonce expires and the merchant will need to try again.

Using the API token

After a successful connection, your app has an access_token (prefixed with apst_). For Appstle-initiated connections (Flow B), the token is returned immediately in the verify response. For partner-initiated connections (Flow A), the token is delivered asynchronously to your /appstle/approved endpoint after the merchant approves (see Handling the approval callback below). Use this token exactly like a merchant API key — pass it in the X-API-Key header:
curl -X GET \
  "https://membership-admin.appstle.com/api/external/v2/subscription-contract-details?shop=cool-store.myshopify.com" \
  -H "X-API-Key: apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012"

Token properties

PropertyDetail
FormatStarts with apst_ followed by 40 alphanumeric characters
ScopeOne token per merchant per partner
PermissionREAD_ONLY or READ_WRITE (set during partner onboarding)
BillingPartner tokens bypass the paid API plan — merchants are never billed for partner API usage
RevocationRevoked instantly when the merchant disconnects or uninstalls Appstle
ExpiryTokens do not expire on their own. They remain valid until explicitly revoked.

Available endpoints

Partner tokens grant access to the same External API endpoints as merchant API keys:
  • Membership contractsGET /api/external/v2/subscription-contract-details (list membership contracts, filter by customer, status, plan)
  • Cancel membershipDELETE /api/external/v2/subscription-contracts/{id} (requires READ_WRITE)
  • Update payment methodPUT /api/external/v2/subscription-contracts-update-payment-method (requires READ_WRITE)
  • Apply discountPUT /api/external/v2/subscription-contracts-apply-discount (requires READ_WRITE)
  • Add discountPUT /api/external/v2/subscription-contracts-add-discount (requires READ_WRITE)
  • Remove discountPUT /api/external/v2/subscription-contracts-remove-discount (requires READ_WRITE)
  • Add line itemPUT /api/external/v2/subscription-contracts-add-line-item (requires READ_WRITE)
See the full Integration guide for complete endpoint documentation.

Data sync (push model)

Some integrations work best when Appstle pushes data to your app, rather than your app pulling from Appstle’s API. For example, an analytics platform might need Appstle to push membership data so it can be tracked alongside other store metrics.

How it works

During onboarding, you can configure a sync_path on your server (e.g., /appstle/sync). When membership events occur (memberships created, cancelled, billing attempts, plan changes, upgrades/downgrades), Appstle calls your endpoint with the relevant data.
Config fieldExampleDescription
sync_path/appstle/syncYour endpoint where Appstle pushes membership data
disconnect_path/appstle/disconnectYour endpoint called when a merchant disconnects

Authentication

When Appstle calls your endpoints, it authenticates using the auth mode configured for your partner:
  • Partner Secret mode: no additional headers (your endpoints are responsible for validating the source — consider IP allowlisting)
  • HMAC-SHA256 mode (recommended): Appstle signs every request with X-Partner-Timestamp and X-Partner-Signature headers. Your app should verify the HMAC signature to confirm the request came from Appstle.
Verifying incoming HMAC signatures (Node.js):
const crypto = require('crypto');

function verifyAppstleSignature(req, hmacKey) {
  const timestamp = req.headers['x-partner-timestamp'];
  const signature = req.headers['x-partner-signature'];

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

  // Reject if timestamp is more than 5 minutes old
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) return false;

  // Compute expected signature
  const data = timestamp + req.rawBody; // make sure you capture raw body
  const expected = crypto
    .createHmac('sha256', hmacKey)
    .update(data)
    .digest('hex');

  // Constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

Pull vs push — which model do I need?

ModelConnect modeYour app calls Appstle?Appstle calls your app?Use case
Pull (default)Nonce HandshakeYes (via apst_ API key)NoHelpdesks, CRMs reading membership data on demand
PushSimple Token ExchangeNoYes (via your token + sync_path)Analytics tools, search platforms that index data
BidirectionalNonce Handshake + sync_pathYesYesFull two-way integrations

Disconnecting

Merchant disconnects from Appstle

Merchants can disconnect your integration anytime from Appstle Dashboard → Settings → Partner Connections. When they do:
  • Your API token for that merchant is revoked immediately
  • Subsequent API calls will return 401 Unauthorized
  • Appstle sends a disconnect webhook to your app (if you configured a disconnect_path during onboarding — see below)
  • Your app should handle this gracefully and show a “Reconnect” option
Best practice: in your API client, check for 401 responses and update your UI to show the connection as disconnected:
async function callAppstleApi(shopDomain, endpoint) {
  const token = await getAccessToken(shopDomain);

  try {
    const response = await axios.get(`${APPSTLE_BASE}${endpoint}`, {
      headers: { 'X-API-Key': token },
    });
    return response.data;
  } catch (err) {
    if (err.response?.status === 401) {
      // Token was revoked — merchant disconnected
      await markAsDisconnected(shopDomain);
      throw new Error('Appstle connection was revoked. Merchant needs to reconnect.');
    }
    throw err;
  }
}
If you configure a disconnect_path during onboarding (e.g., /appstle/disconnect), Appstle will call your endpoint whenever a merchant disconnects — whether they disconnect from the Appstle dashboard, from your app, or by uninstalling Appstle entirely. Request body from Appstle:
{
  "shop_domain": "cool-store.myshopify.com"
}
If your partner uses HMAC-SHA256 auth, the webhook includes signed headers (X-Partner-Timestamp, X-Partner-Signature) so you can verify it came from Appstle. Your app should:
  1. Mark the connection as disconnected in your database
  2. Clean up any stored Appstle tokens for this shop
  3. Return any 2xx status code
Node.js (Express)
router.post('/appstle/disconnect', async (req, res) => {
  const { shop_domain } = req.body;

  // Optional: verify HMAC signature if using HMAC auth
  // const isValid = verifyHmacSignature(req);
  // if (!isValid) return res.status(401).json({ error: 'Invalid signature' });

  // Clean up the connection
  await db.appstleTokens.delete({ shopDomain: shop_domain });
  await db.connections.update(
    { shopDomain: shop_domain },
    { status: 'disconnected', disconnectedAt: new Date() }
  );

  res.json({ success: true });
});
This is a best-effort notification — your app should also handle 401 responses from the Appstle API as a fallback signal that the connection was revoked.

Partner disconnects programmatically

Your app can disconnect a merchant using your partner authentication (Partner Secret or HMAC-SHA256):
curl -X POST "https://membership-admin.appstle.com/api/partner/your-partner-id/disconnect" \
  -H "X-Partner-Secret: YOUR_PARTNER_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "shop_domain": "cool-store.myshopify.com"
  }'
Response:
{
  "success": true
}

Check connection status

Verify a merchant’s connection status. Partners can authenticate with their Partner Secret or HMAC signature:
# With Partner Secret
curl -X GET "https://membership-admin.appstle.com/api/partner/your-partner-id/status?shop_domain=cool-store.myshopify.com" \
  -H "X-Partner-Secret: your-partner-secret"

# With HMAC
curl -X GET "https://membership-admin.appstle.com/api/partner/your-partner-id/status?shop_domain=cool-store.myshopify.com" \
  -H "X-Partner-Timestamp: $TIMESTAMP" \
  -H "X-Partner-Signature: $SIGNATURE"
Response (active connection):
{
  "partner_id": "your-partner-id",
  "shop_domain": "cool-store.myshopify.com",
  "status": "active",
  "connected_at": "2026-03-07T20:30:00Z"
}
Response (pending merchant approval):
{
  "partner_id": "your-partner-id",
  "shop_domain": "cool-store.myshopify.com",
  "status": "pending_merchant_approval"
}
Other possible status values:
StatusMeaning
activeConnected and working — API token is valid
pending_merchant_approvalPartner-initiated connect is awaiting merchant approval
rejectedMerchant rejected the connection request
expiredThe pending request expired (30-day window) — partner must initiate a new connection
not_connectedNo connection exists for this partner + shop
Use the status endpoint to poll for approval if your app doesn’t implement the /appstle/approved callback. Poll every 30–60 seconds after initiating a connect. Once the status changes from pending_merchant_approval to active, your token has been delivered via the approval callback (or you can request it again).

Handling the approval callback

When a merchant approves a partner-initiated connection, Appstle delivers the API token by calling an endpoint on your server. This applies to partner-initiated connections only — Appstle-initiated connections (Flow B) return the token immediately.

Endpoint: POST /appstle/approved

The path defaults to /appstle/approved but can be customized during onboarding (configured as approval_callback_path).
Your /appstle/approved endpoint must accept unauthenticated POST requests from Appstle’s servers. Do not put authentication middleware (e.g., JWT validation, API key checks) on this endpoint — Appstle will not send your app’s auth credentials when calling this callback. If you need to verify the request is from Appstle, use HMAC-SHA256 authentication mode — when enabled, the callback includes signed headers (X-Partner-Timestamp, X-Partner-Signature) you can verify.
Request body from Appstle (Nonce Handshake mode):
{
  "shop_domain": "cool-store.myshopify.com",
  "access_token": "apst_AbCdEfGhIjKlMnOpQrStUvWxYz123456789012"
}
Request body from Appstle (Simple Token Exchange mode):
{
  "shop_domain": "cool-store.myshopify.com",
  "status": "approved"
}
In Simple Token Exchange mode, your app already provided its own token during the connect call. The approval callback simply confirms the connection is now active — Appstle will start using your token for API calls.
Expected response: Return any 2xx status code with a JSON body (e.g., { "success": true }). If your endpoint returns a non-2xx status (e.g., 401 Unauthorized), the connection is still approved on Appstle’s side, but your app won’t know — see What if the callback fails? below.
If your partner uses HMAC-SHA256 auth, the callback includes signed headers (X-Partner-Timestamp, X-Partner-Signature) so you can verify it came from Appstle.
router.post('/appstle/approved', async (req, res) => {
  const { shop_domain, access_token, status } = req.body;

  // Optional: verify HMAC signature if using HMAC auth
  // if (!verifyAppstleSignature(req, HMAC_KEY)) {
  //   return res.status(401).json({ error: 'Invalid signature' });
  // }

  if (access_token) {
    // Nonce Handshake mode — store the Appstle API token
    await saveToken(shop_domain, access_token);
    console.log(`Connection approved for ${shop_domain} — token received`);
  } else if (status === 'approved') {
    // Simple Token Exchange mode — our token is now active
    await markConnectionActive(shop_domain);
    console.log(`Connection approved for ${shop_domain} — our token is now active`);
  }

  res.json({ success: true });
});

What if the callback fails?

If your endpoint is unreachable or returns an error, the connection is still approved on Appstle’s side. The API token exists and is valid. Your app can:
  1. Poll the status endpoint — check GET /api/partner/{id}/status?shop_domain=... until the status is active
  2. Retry from Appstle’s side — currently, Appstle does not automatically retry the callback. Contact support if you need the token re-delivered.

What if the merchant rejects?

If the merchant clicks “Reject,” the connection status changes to rejected and Appstle notifies your app via the disconnect webhook (if configured). Your app should handle this gracefully — show the merchant that the connection was not approved.

Error handling

All partner endpoints return structured error responses:
{
  "type": "https://membership-admin.appstle.com/problem",
  "title": "Bad Request",
  "status": 400,
  "detail": "UserGeneratedError:Active connection already exists. Disconnect first.",
  "errorKey": "ALREADY_CONNECTED"
}

Error codes

Error codeHTTP statusWhen it happensWhat to do
PARTNER_NOT_FOUND400Your Partner ID is wrong, or the partner has been deactivatedDouble-check your Partner ID. Contact Appstle if unexpected.
TOKEN_INVALID400Auth failed: X-Partner-Secret is wrong, or HMAC signature is invalid, or timestamp is >5 min offVerify your secret or HMAC key. Check for trailing whitespace. For HMAC: ensure server clock is synced (NTP) and you’re signing timestamp + body exactly.
SHOP_NOT_FOUND400The shop doesn’t have Appstle Memberships installedTell the merchant to install Appstle Memberships first.
ALREADY_CONNECTED400An active connection already exists for this partner + shopCall disconnect first, then reconnect. Or skip — you’re already connected.
VERIFICATION_FAILED400Nonce didn’t match, expired (>5 min), or your /verify endpoint returned falseGenerate a fresh nonce and try again. Check your nonce storage logic.
NOT_CONNECTED400Trying to disconnect or check status, but no active connection existsThe merchant may have already disconnected from their side.
PARTNER_UNREACHABLE400Appstle couldn’t reach your /appstle/connect or /appstle/verify endpointCheck your endpoint URL is correct, HTTPS, and publicly accessible. Check your server logs.

Security checklist

Before going live, verify all of these:
  • Partner Secret / HMAC key is stored in environment variables or a secrets manager — not hardcoded in source code
  • Nonces are generated using a cryptographically secure random generator (crypto.randomBytes, secrets.token_hex, SecureRandom, etc.)
  • Nonces are stored with a TTL (≤ 5 minutes) and deleted after verification
  • Nonces are compared using a constant-time comparison to prevent timing attacks (most frameworks do this by default for string equality)
  • Endpoints are served over HTTPS — Appstle will not call HTTP endpoints
  • shop_domain is validated in your /appstle/connect and /appstle/verify endpoints — reject domains you don’t recognize
  • Access tokens are stored encrypted at rest (or in a secrets manager)
  • 401 responses are handled gracefully — show a “Reconnect” option, don’t break silently
  • Error responses from Appstle are logged for debugging
  • (HMAC only) Server clock is synced via NTP — timestamps more than 5 minutes off will be rejected
  • (If using disconnect webhook) Your /appstle/disconnect endpoint cleans up stored tokens and marks the connection as inactive

Complete example: partner-initiated flow (Node.js)

Here’s a full, copy-pasteable implementation of Flow A in Express:
// appstle-partner.js
const express = require('express');
const crypto = require('crypto');
const axios = require('axios');
const Redis = require('ioredis');

const router = express.Router();
const redis = new Redis(process.env.REDIS_URL);

const PARTNER_ID = process.env.APPSTLE_PARTNER_ID;
const PARTNER_SECRET = process.env.APPSTLE_PARTNER_SECRET;
const APPSTLE_BASE = process.env.APPSTLE_BASE_URL || 'https://membership-admin.appstle.com';
const NONCE_TTL = 300; // 5 minutes in seconds

// ──────────────────────────────────────────────
// Nonce helpers
// ──────────────────────────────────────────────

async function storeNonce(shopDomain, nonce) {
  await redis.set(`appstle:nonce:${shopDomain}`, nonce, 'EX', NONCE_TTL);
}

async function verifyAndDeleteNonce(shopDomain, nonceToCheck) {
  const key = `appstle:nonce:${shopDomain}`;
  const stored = await redis.get(key);
  if (!stored || stored !== nonceToCheck) return false;
  await redis.del(key);
  return true;
}

// ──────────────────────────────────────────────
// Token storage (use your database in production)
// ──────────────────────────────────────────────

async function saveToken(shopDomain, accessToken) {
  // In production: encrypt the token before storing
  await redis.set(`appstle:token:${shopDomain}`, accessToken);
}

async function getToken(shopDomain) {
  return redis.get(`appstle:token:${shopDomain}`);
}

// ──────────────────────────────────────────────
// Flow A: Partner-initiated connect
// Called when merchant clicks "Connect Appstle" in YOUR dashboard
// ──────────────────────────────────────────────

router.post('/connect-appstle', async (req, res) => {
  const { shopDomain } = req.body;

  try {
    // 1. Generate nonce
    const nonce = crypto.randomBytes(32).toString('hex');
    await storeNonce(shopDomain, nonce);

    // 2. Call Appstle
    const response = await axios.post(
      `${APPSTLE_BASE}/api/partner/${PARTNER_ID}/connect`,
      { shop_domain: shopDomain, callback_nonce: nonce },
      { headers: { 'X-Partner-Secret': PARTNER_SECRET, 'Content-Type': 'application/json' } }
    );

    // 3. Connection is now pending merchant approval
    if (response.data.status === 'pending_merchant_approval') {
      // Mark as pending in your system — show the merchant a "waiting for approval" state
      await redis.set(`appstle:pending:${shopDomain}`, 'true');
      return res.json({ pending: true, message: 'Waiting for merchant to approve in Appstle dashboard' });
    }

    res.status(400).json({ error: 'Connection failed' });
  } catch (err) {
    const detail = err.response?.data?.detail || err.message;
    console.error('Appstle connect failed:', detail);
    res.status(400).json({ error: detail });
  }
});

// ──────────────────────────────────────────────
// Endpoint: POST /appstle/verify
// Called BY Appstle during Flow A to verify your nonce
// ──────────────────────────────────────────────

router.post('/appstle/verify', async (req, res) => {
  const { shop_domain, callback_nonce } = req.body;
  const verified = await verifyAndDeleteNonce(shop_domain, callback_nonce);
  res.json({ verified });
});

// ──────────────────────────────────────────────
// Endpoint: POST /appstle/approved
// Called BY Appstle when merchant approves a partner-initiated connection
// ──────────────────────────────────────────────

router.post('/appstle/approved', async (req, res) => {
  const { shop_domain, access_token, status } = req.body;

  if (access_token) {
    // Nonce Handshake mode — Appstle is delivering our API token
    await saveToken(shop_domain, access_token);
    await redis.del(`appstle:pending:${shop_domain}`);
    console.log(`Approved! Token received for ${shop_domain}`);
  } else if (status === 'approved') {
    // Simple Token Exchange mode — our token is now active on Appstle's side
    await redis.del(`appstle:pending:${shop_domain}`);
    console.log(`Approved! Our token is now active for ${shop_domain}`);
  }

  res.json({ success: true });
});

// ──────────────────────────────────────────────
// Endpoint: POST /appstle/connect
// Called BY Appstle during Flow B (Appstle-initiated)
// ──────────────────────────────────────────────

router.post('/appstle/connect', async (req, res) => {
  const { shop_domain, callback_url, callback_nonce } = req.body;

  // Verify the shop exists in your system
  // const shop = await db.shops.findOne({ domain: shop_domain });
  // if (!shop) return res.status(400).json({ error: 'Unknown shop' });

  // Auto-approve: immediately call back to complete the handshake
  // (Flow B doesn't need merchant approval — merchant initiated it from Appstle)
  try {
    const response = await axios.post(callback_url, {
      shop_domain,
      callback_nonce,
    }, {
      headers: { 'X-Partner-Secret': PARTNER_SECRET, 'Content-Type': 'application/json' },
    });

    if (response.data.verified && response.data.access_token) {
      await saveToken(shop_domain, response.data.access_token);
    }
  } catch (err) {
    console.error('Failed to complete Appstle handshake:', err.message);
  }

  res.json({ success: true });
});

module.exports = router;

Complete example: partner-initiated flow (Python)

# appstle_partner.py
import os
import secrets
import redis
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)
r = redis.Redis.from_url(os.environ.get("REDIS_URL", "redis://localhost:6379"))

PARTNER_ID = os.environ["APPSTLE_PARTNER_ID"]
PARTNER_SECRET = os.environ["APPSTLE_PARTNER_SECRET"]
APPSTLE_BASE = os.environ.get("APPSTLE_BASE_URL", "https://membership-admin.appstle.com")
NONCE_TTL = 300  # 5 minutes


# ── Nonce helpers ──

def store_nonce(shop_domain: str, nonce: str):
    r.set(f"appstle:nonce:{shop_domain}", nonce, ex=NONCE_TTL)

def verify_and_delete_nonce(shop_domain: str, nonce_to_check: str) -> bool:
    key = f"appstle:nonce:{shop_domain}"
    stored = r.get(key)
    if not stored or stored.decode() != nonce_to_check:
        return False
    r.delete(key)
    return True


# ── Token storage ──

def save_token(shop_domain: str, access_token: str):
    r.set(f"appstle:token:{shop_domain}", access_token)

def get_token(shop_domain: str):
    val = r.get(f"appstle:token:{shop_domain}")
    return val.decode() if val else None


# ── Flow A: Partner-initiated connect ──

@app.route("/connect-appstle", methods=["POST"])
def connect_appstle():
    shop_domain = request.json["shopDomain"]

    # 1. Generate nonce
    nonce = secrets.token_hex(32)
    store_nonce(shop_domain, nonce)

    # 2. Call Appstle
    resp = requests.post(
        f"{APPSTLE_BASE}/api/partner/{PARTNER_ID}/connect",
        json={"shop_domain": shop_domain, "callback_nonce": nonce},
        headers={"X-Partner-Secret": PARTNER_SECRET, "Content-Type": "application/json"},
    )
    resp.raise_for_status()
    data = resp.json()

    # 3. Connection is pending merchant approval
    if data.get("status") == "pending_merchant_approval":
        r.set(f"appstle:pending:{shop_domain}", "true")
        return jsonify({"pending": True, "message": "Waiting for merchant to approve in Appstle dashboard"})

    return jsonify({"error": "Connection failed"}), 400


# ── Endpoint: POST /appstle/verify (called BY Appstle during Flow A) ──

@app.route("/appstle/verify", methods=["POST"])
def appstle_verify():
    body = request.json
    verified = verify_and_delete_nonce(body["shop_domain"], body["callback_nonce"])
    return jsonify({"verified": verified})


# ── Endpoint: POST /appstle/approved (called BY Appstle when merchant approves) ──

@app.route("/appstle/approved", methods=["POST"])
def appstle_approved():
    body = request.json
    shop_domain = body["shop_domain"]
    access_token = body.get("access_token")
    status = body.get("status")

    if access_token:
        # Nonce Handshake mode — store the Appstle API token
        save_token(shop_domain, access_token)
    elif status == "approved":
        # Simple Token Exchange mode — our token is now active
        pass  # mark connection as active in your DB

    r.delete(f"appstle:pending:{shop_domain}")
    return jsonify({"success": True})


# ── Endpoint: POST /appstle/connect (called BY Appstle during Flow B) ──

@app.route("/appstle/connect", methods=["POST"])
def appstle_connect():
    body = request.json
    shop_domain = body["shop_domain"]
    callback_url = body["callback_url"]
    callback_nonce = body["callback_nonce"]

    # Auto-approve: call back immediately
    # (Flow B doesn't need merchant approval — merchant initiated it from Appstle)
    try:
        resp = requests.post(
            callback_url,
            json={"shop_domain": shop_domain, "callback_nonce": callback_nonce},
            headers={"X-Partner-Secret": PARTNER_SECRET, "Content-Type": "application/json"},
        )
        data = resp.json()
        if data.get("verified") and data.get("access_token"):
            save_token(shop_domain, data["access_token"])
    except Exception as e:
        app.logger.error(f"Handshake failed: {e}")

    return jsonify({"success": True})

FAQ

Yes. Each partner gets its own scoped API token. Merchants can connect as many partners as they want. The tokens are completely independent.
All active partner connections for that shop are automatically disconnected. Your tokens will stop working (401 responses).
Yes — contact the Appstle team. New connections will use the updated permission, but existing connections keep their original permission until reconnected.
Partner tokens share the same rate limits as regular API keys. If you receive a 429 Too Many Requests, implement exponential backoff.
Use a Shopify development store with Appstle Memberships installed. The partner integration works identically in development and production. You can use a tool like ngrok to expose your local endpoints to the internet for testing.
The merchant simply needs to click “Connect” again. A new nonce will be generated. Old nonces are automatically cleaned up.
Yes. Flow A is for when the merchant connects from your dashboard. Flow B is for when they connect from Appstle’s dashboard. Both are needed for a complete integration. You also need the /appstle/approved endpoint to receive API tokens after merchant approval (Flow A).
No. A new connect handshake requires the merchant to initiate it from one of the dashboards. This is by design — merchants must explicitly authorize each connection.
For security and trust. When your app initiates a connection, the merchant hasn’t explicitly agreed on Appstle’s side. The approval step ensures merchants consciously grant API access to partner apps. Appstle-initiated connections (Flow B) skip this step because the merchant is already clicking “Connect” in the Appstle dashboard.
Pending connection requests expire after 30 days. If the merchant doesn’t approve or reject within that window, the request expires and your app will need to initiate a new connection.
The status changes to rejected and your app is notified via the disconnect webhook (if configured). The merchant can be asked to reconnect later if they change their mind — your app can initiate a new connection request.

Need help?