Applications
Applications — /api/org/{slug}/applications
Section titled “Applications — /api/org/{slug}/applications”| Method · Route | Purpose | Source |
|---|---|---|
GET / | List applications | ApplicationEndpoints.cs:22 |
GET /{appId} | Get one application | :35 |
POST / | Create application | :49 |
PUT /{appId} | Update application (incl. OIDC/SAML client config) | :82 |
DELETE /{appId} | Delete application | :128 |
PATCH /{appId}/status | Toggle status | :160 |
POST /{appId}/rotate-secret | Rotate client secret | :187 |
An application’s public key is its client key (client_..., one per platform × environment) — it is what the SDK’s clientId and the hosted login URL carry. OIDC client config (PKCE requirement, allowed grant types, scopes, post-logout redirects) and SAML SP registration (EntityID, ACS URL) live on the Application — see OIDC and SAML.
App groups — /api/org/{slug}/app-groups
Section titled “App groups — /api/org/{slug}/app-groups”An App Group is the product axis: it holds hosted-login branding (colors, social providers, passkey/magic-link toggles, hero copy), the Passkey RP ID, and — when the group sells something — its storefront and payout links.
| Method · Route | Purpose | Source |
|---|---|---|
GET / · GET /{groupId} | List / get | AppGroupEndpoints.cs:24, :37 |
POST / · PUT /{groupId} · DELETE /{groupId} | App-group CRUD (branding, login methods, hero) | :51, :93, :245 |
POST /{groupId}/apps | Add app to group | :294 |
POST / with monetizationEnabled: true | Create the group with Axowl Payments on (collection through App Group Payments on the marketplace). Omitted or false = the group connects its own payment gateway (KG Inicis, Toss Payments, Razorpay and others) while Axowl runs products, subscriptions, vouchers and entitlements; the gateway connection UI follows. The dashboard picks the default from the organization’s country: in the 44 countries where Stripe opens seller accounts it defaults to Axowl Payments, elsewhere Axowl Payments is shown as not yet available, and in Korea only your own gateway is offered. Turning it on at creation passes the same two checks as PUT — owner-only (view.billing, else 403 PERMISSION_DENIED) and the plan’s collecting-group limit (else 403 UPGRADE_REQUIRED). | AppGroupEndpoints.cs |
POST · DELETE /{groupId}/icon | App-group icon. PNG, JPEG, WebP or SVG up to 2MB. The type is read from the file’s bytes and an SVG is cleaned before it is stored. | AppGroupIconEndpoints.cs:17, :90 |
Commerce (owner-only, view.billing gate)
Section titled “Commerce (owner-only, view.billing gate)”These routes lead to money (payout accounts, prices), so they sit behind the same owner-only gate as billing — hiding a button in the dashboard is not enough.
| Method · Route | Purpose | Source |
|---|---|---|
POST /{groupId}/gateway-linkup | Register the app group as a brand with the partner storefront (Bullmark). The partner app comes from configuration, never from the request. Refused for a group with no products or on the plan floor. | AppGroupEndpoints.cs:349 |
POST /{groupId}/gateway-listing | One-time session to the brand’s listing screen (review status, voucher economy). No re-login; the destination is fixed server-side. | :441 |
POST /{groupId}/payout-session | Embedded payout onboarding (Stripe Connect) — returns a short-lived client secret, never cached. Brand slug is resolved server-side, not accepted from the caller. | :505 |
GET /{groupId}/payout-status | Onboarding progress and remaining requirements. | :632 |
POST /{groupId}/products · GET /{groupId}/products | Create a price / list what the group sells. Reading is open to any member who can see the group; writing a price is owner-only. | :731, :796 |
POST /{groupId}/products/{productId}/sync | Push a price to the partner storefront; idempotent (already-registered rows are not re-sent). | :843 |
POST /{groupId}/products/import | Import the partner’s existing prices; skips ones already present (by ProductKey, else name). | :907 |
GET /{groupId}/products/template.xlsx | Download the bulk-registration template (Products sheet with headers and two example rows, Guide sheet). Example currency is KRW when the group has its own gateway connected. Owner-only. | AppGroupProductBulkEndpoints |
POST /{groupId}/products/bulk | Register many prices at once from a multipart file (.xlsx or .csv, up to 200 rows, 2MB). Columns: name, type (Subscription · OneTimeAddon · Usage), currency (USD · KRW), price, yearly_discount_percent, price_url, product_key, usage_unit_label, description. Each row goes through the same path as POST /{groupId}/products; a bad row does not stop the others. Returns { total, created, registered, drafts, failed, rows: [{ row, name, status, detail }] }. Owner-only. | AppGroupProductBulkEndpoints |
POST /{groupId}/products/{productId}/reprice | Change a price, and optionally name, description, priceUrl and usageUnitLabel (omit a field to keep it; the key and type cannot change — to change the key, delete the product and create it again). Responds changed: false without touching anything when nothing differs. A registered product is withdrawn and published again; the new one is reported as “not registered” until re-synced. | :1010 |
DELETE /{groupId}/products/{productId} | Remove a price (owner-only). | :1099 |
Own payment gateway (owner-only, view.billing gate, passkey seal)
Section titled “Own payment gateway (owner-only, view.billing gate, passkey seal)”For organizations outside the 44 countries where Stripe opens seller accounts (Korea first), a brand can connect its own payment gateway instead of Axowl Payments: customers pay straight into the brand’s gateway account while Axowl runs products, subscriptions, vouchers and entitlements. Korean gateways connect through PortOne (KG Inicis, Toss Payments, NHN KCP, NICE, Smartro, KSNET, KPN, Welcome Payments, KICC, plus Kakao Pay, Naver Pay and Toss Pay).
| Method · Route | Purpose | Source |
|---|---|---|
GET /{groupId}/payment-gateway | Current connection: gateway, store id, channel keys, webhook URL. Secrets are never returned. | AppGroupPaymentGatewayEndpoints.cs |
POST /{groupId}/payment-gateway | Connect: pgProvider, storeId, paymentChannelKey, optional billingChannelKey, apiSecret, webhookSecret (whsec_…), isTest, and assertionResponseJson (passkey seal bound to this group). The API secret is checked against PortOne before anything is stored; secrets are stored encrypted. Replaces the previous connection. | |
POST /{groupId}/payment-gateway/disconnect | Disconnect (passkey seal required). The brand’s webhook URL stops working at once. | |
POST /api/v1/webhooks/portone/brand/{token} | Per-brand PortOne webhook. Verified with that brand’s webhook secret (Standard Webhooks, ±5 min), idempotent per webhook-id, and never trusted on its own: Axowl re-reads the payment with the brand’s API secret and only a PAID payment of that store is recorded and forwarded to the marketplace ledger. | WebhookEndpoints.cs |
Events appgroup.payment_gateway.connected / .disconnected (AGG001/AGG002) are sealed in the audit timeline; they carry the gateway id and store id, never a secret.
What changes once a gateway is connected. Connecting also tells the marketplace ledger which store and channel the brand sells through (no secrets travel); if the ledger cannot be told, nothing is connected and the call returns 502 GATEWAY_MIRROR_FAILED. From then on:
- Prices in won.
POST /{groupId}/productsacceptscurrency—USD(default) orKRW.KRWis allowed only while a gateway is connected (409 GATEWAY_NOT_LINKEDotherwise) and must be a whole number of won; theretailPriceUsdfield then carries won (the field name is kept — it is the sealed name on the ledger).GET …/productsreturnscurrencyper row. - Checkout answers differ.
POST /api/org/{slug}/marketplace/apps/{groupKey}/checkoutreturnsprovider: "portone"and anintent(paymentId,orderId,orderName,amount,currency,storeId,channelKey,billingChannelKey,issueBillingKey,mode,billingCycle) instead of a StriperedirectUrl. The buyer’s browser opens the PortOne payment window with those values; one-time purchases are paid in the window, subscriptions register a card (billing key). - Subscriptions.
POST /api/org/{slug}/marketplace/apps/{groupKey}/own-gateway/billing-keywithorderId,paymentIdand thebillingKeythe window returned (buyer’s owner or admin). Axowl checks the key with the brand’s gateway, stores it encrypted, charges the first period and records the payment in the same ledger as the webhook. Renewals are charged by Axowl every period with that key; a declined card marks the subscription past due on the marketplace and the installed app ends after 21 unpaid days. Plan changes are not available for these subscriptions yet (409 NOT_SUPPORTED_FOR_OWN_GATEWAY); cancel and subscribe to the other plan. - Payment confirmations reach the marketplace as
external-paidnotifications; the brand’s own gateway handles refunds and chargebacks. Axowl bills the brand an engine fee on the processed volume (accrued per payment; invoicing follows).
Collection and settlement rules for what an app group sells are in Billing & Monetization.
Marketplace (owner or admin, passkey seal)
Section titled “Marketplace (owner or admin, passkey seal)”An app group can be listed on marketplace.axowl.com. The owner decides here; nothing is listed until it is published. The marketplace lists web apps (SaaS and PaaS) only. The server checks these conditions every time you publish:
- Plan. The organization is on the Pro plan or higher (feature
feature.marketplace.listing). Publishing, updating the listing and translating return403 UPGRADE_REQUIREDon Free. Unpublishing works on any plan. If an organization moves below Pro, its apps drop out of the public list until it upgrades again. - Service model and field. The app group is marked SaaS or PaaS and has a field (category) chosen.
- The app group has at least one active Web application. Groups with only mobile, desktop or server-to-server apps cannot be listed, and a published group drops out of the list if its last web app is removed.
- A domain for this app group is verified in Axowl (the Custom Domain card).
- The app address you enter is on that company domain —
app.acme.comwhenlogin.acme.comis verified. Under a two-letter country domain the common second level (co,com,or,ne,ac,go…) is kept, soacme.co.krnever widens toco.kr. - The app address opens over
https(no 4xx/5xx). The request goes through the SSRF-guarded client.
Registering payments is not a condition.
| Method · Route | Purpose | Source |
|---|---|---|
GET /{groupId}/marketplace | Current app address, publish time, verified domain, screenshots | AppGroupMarketplaceEndpoints.cs:37 |
POST /{groupId}/marketplace/check | Run the four checks for an address without publishing. Returns each check as passed, failed or not reached. | :38 |
PUT /{groupId}/marketplace | Publish or unpublish. Body: appUrl, publish, assertionResponseJson, and optionally translations (below). The seal is bound to this group and to publish-or-unpublish (step-up purpose appgroup.marketplace). A publish that fails a check returns 409 CONDITIONS_NOT_MET with the checks. | :39 |
POST /{groupId}/marketplace/translate | AI translation draft for review. Returns source (the texts to translate) and langs (language → the same keys, translated). Pro or higher. The model is called only when the source changed since the last translation, at most 10 times a day per app group (429 TRANSLATE_LIMIT); otherwise the saved draft is returned with cached: true. 400 NOTHING_TO_TRANSLATE when the app group has no description. | AppGroupMarketplaceEndpoints.cs |
POST /{groupId}/marketplace/screenshots | Add a screenshot or a mockup to the listing (multipart field image). Up to 3 per app group, under 3MB each. PNG, JPEG, WebP or SVG. The type is read from the file’s bytes, not from the declared content type, and an SVG is cleaned before it is stored (scripts, event attributes, javascript: links and foreignObject are removed). Saved at once, no seal. 409 SCREENSHOTS_FULL, 400 SCREENSHOT_TYPE, 400 SCREENSHOT_TOO_LARGE. Returns screenshots. | AppGroupMarketplaceEndpoints.cs |
DELETE /{groupId}/marketplace/screenshots/{index} | Remove one screenshot by its position (0-based). Returns the remaining screenshots. 404 when the position does not exist. | AppGroupMarketplaceEndpoints.cs |
GET /{groupId}/marketplace/sales | Payments for this app, one line per payment: gross, take rate, marketplace commission, net, first payment or renewal. Query days (default 30, up to 365). | AppGroupMarketplaceEndpoints.cs |
The listing uses the app group’s name, description and icon. Up to three screenshots or mockups, uploaded on the Marketplace card, are shown at the top of the product page. If the app group has a Bullmark introduction that has passed review, its text, logo and photos are added. Bullmark does not decide what is listed.
Translation, reviewed before it goes live. Publishing and updating the listing start with a translation into eight languages (en, ko, ja, zh-Hant, de, es, fr, pt). The texts translated are the description, the Bullmark introduction and details, and each plan’s description. The app name and plan names are fixed values and are not translated; plan names and descriptions are edited on the Products tab, the introduction in Bullmark. The owner can also rewrite the description during review: /translate accepts description (not saved yet) and translates that text, and the sealed publish saves it through the normal app group update. The owner reviews each language, edits it or leaves a field empty (that field then shows the original), and sends the result as translations, with description, in the sealed publish. The draft from /translate is never shown on the marketplace on its own. If the source text changes later, the public list shows the original until the listing is updated again.
Published apps are readable without authentication at GET /api/public/marketplace/apps (cached for five minutes; publishing or unpublishing clears the cache) — AppGroupMarketplaceEndpoints.cs:41. Each app carries plans: the products you registered on the app group that can be bought there (connected to payments, price above zero, type Subscription or OneTimeAddon). An empty list means the app is free. Each app also carries i18n: language → description, introDescription, details, plan.{id}.description (plan id without dashes), present only when the owner reviewed the translation and the source has not changed since. A missing key means the original text. Each app also carries screenshots (up to three image URLs, empty when none were uploaded) and publisherSlug (the selling organization).
Across your organizations. The console’s System home has a Marketplace tab with two lists. Both cover every organization where you are an active owner or admin; other organizations are not included. Publishing from there opens the same Marketplace card, so the checks and the seal above are unchanged.
| Method · Route | Purpose | Source |
|---|---|---|
GET /api/me/marketplace/apps | Per organization (slug, name), its app groups with id, key, name, iconUrl, brandColor, webApp (name of an active web app, or null), verifiedDomain, appUrl, publishedAt. webApp and verifiedDomain follow checks 0 and 1; they are a hint, and the server runs all four checks again when you publish. | AppGroupMarketplaceEndpoints.cs |
GET /api/me/marketplace/subscriptions | Per organization, what it turned on or installed from the marketplace: kind (service for an Axowl service, app for another organization’s app), key (app: + group key for apps), name, publisher, openUrl, productId (the product page id on the marketplace), enabledAt, paid. Connectors such as Slack are not included. | AppGroupMarketplaceEndpoints.cs |
Both return 401 when the token has no user.
Selling on the marketplace
Section titled “Selling on the marketplace”Another organization installs your app from its product page. The price comes from the products you register on the app group. The marketplace does not keep its own price list.
- Free app (no plans): an owner or admin of the buying organization installs it with a passkey seal, using the same call as turning on any integration.
- Paid app: turning it on directly returns
402 PAYMENT_REQUIRED. The buyer picks a plan, can enter a voucher code, seals with a passkey and pays by card. The app is installed when the payment is confirmed. A voucher applies to the first payment only.Subscriptionplans charge every month until canceled.OneTimeAddonplans charge once. - After installing: the buyer can move to another
Subscriptionplan of the same app (the difference is charged at once, prorated) and can buy aOneTimeAddonwhile the app stays installed. - Uninstalling a paid app ends the subscription at the end of the period already paid for. The app stays installed until that date. If there is no running subscription, it is removed at once.
Money goes to your own Stripe account. The marketplace commission is taken at the source, in addition to your plan’s take rate. See Billing & Monetization.
| Method · Route | Purpose | Source |
|---|---|---|
POST /api/org/{slug}/marketplace/apps/{groupKey}/checkout | Start a purchase for the organization in slug. Body: productId, voucherCode (optional), assertionResponseJson. The seal is the same one as installing (integration.enable, subject slug|app:{groupKey}|enable). Returns redirectUrl (the card payment page), mode, priceUsd, firstAmountUsd. 409 ALREADY_INSTALLED when the app is already installed and the product is a Subscription; a OneTimeAddon can be bought while installed. | AppGroupMarketplacePurchaseEndpoints.cs |
POST /api/org/{slug}/marketplace/apps/{groupKey}/billing-portal | Owner or admin of the buying organization. Opens the card-payment provider’s billing page for this app’s subscription: change the card, see receipts, cancel (at the end of the period) or take a cancellation back. Body: assertionResponseJson — the same seal as uninstalling (integration.enable, subject slug|app:{groupKey}|disable). Returns url. 404 NO_ACTIVE_SUBSCRIPTION for a one-time or free install. | AppGroupMarketplacePurchaseEndpoints.cs |
GET /api/org/{slug}/marketplace/apps/{groupKey}/subscription | Owner or admin of the buying organization. The plan this organization is subscribed to (subscription, with productId) and the one-time add-ons it bought (addons). subscription is null for a free or one-time install. | AppGroupMarketplacePurchaseEndpoints.cs |
POST /api/org/{slug}/marketplace/apps/{groupKey}/change-plan | Move the running subscription to another Subscription plan of the same app. Body: productId, assertionResponseJson — the same seal as uninstalling (integration.enable, subject slug|app:{groupKey}|disable). The difference is invoiced at once, prorated. 404 NOT_INSTALLED, 404 PRODUCT_NOT_AVAILABLE, 409 SAME_PLAN. | AppGroupMarketplacePurchaseEndpoints.cs |
GET /api/org/{slug}/marketplace/revenue | Owner or admin of the selling organization. What buyers paid for this organization’s apps and vouchers: thisMonth, thisYear and total (each grossUsd and netUsd), byDay (30 days), byMonth (12 months), byYear, byApp and byProduct. gatewayOk: false with zeros when payments cannot be read. See Billing & Monetization. | AppGroupMarketplaceEndpoints.cs |
DELETE /api/org/{slug}/integrations/app:{groupKey}/enable | Uninstall. For a running subscription this answers 202 with endsAt and leaves the app installed until then. If payments cannot be reached it answers 502 SUBSCRIPTION_CANCEL_FAILED and changes nothing. | IntegrationModuleEnabledEndpoints.cs |
POST /api/public/v1/marketplace/purchases | Payment engine only (platform key). event: paid installs the app for buyerOrgId; event: past_due (a renewal failed) leaves it installed and notifies both sides; event: canceled (subscription ended or refunded) removes it. Safe to resend. canceled is accepted after the app group or the buying organization was deleted; paid is not. | PublicApiEndpoints.cs |
POST /api/public/v1/marketplace/issuers/status | Payment engine only (platform key). Asks whether an app’s publisher, or a buying organization, still exists. Body: issuers[] with groupKey, orgId, or both (up to 200). Answers, in the same order, status: active, suspended, deleted or unknown, and a reason (app_group_deleted, org_deleted, org_suspended) when it is not active. No names or plans are returned. | PublicApiEndpoints.cs |
GET /api/org/{slug}/app-groups/{groupId}/marketplace/customers | Owner or admin of the selling organization. The organizations that installed this app, now or before: name, installed or removed, paid or free, since, until. The same list appears on the Marketplace card in the console. | AppGroupMarketplaceEndpoints.cs |
Emails. Access events are sent by Axowl as an in-app notification and an email:
- App installed: to the owners and admins of the buying organization, and to the owners of the selling organization (“new customer”).
- App removed: to the same people.
- Buying organization deleted: its running marketplace subscriptions are canceled by the payment engine and end at the end of the period already paid for. The person who paid gets the cancellation email.
- App deleted by its publisher: to the owners and admins of every organization that had it installed. The install is removed at once. A running subscription is canceled by the payment engine and ends at the end of the period already paid for.
- Listed on or taken off the marketplace: to the owners of the selling organization.
Money events are sent by the payment engine to the person who paid and to the seller:
- Receipts, including every monthly renewal
- A failed renewal, with a link to pay. Axowl also sends an in-app notification to the owners and admins of both organizations.
- A plan change, with the prorated amount. Both organizations also get an in-app notification.
- A cancellation that is scheduled, with the end date
- The subscription ending
- Refunds
Test tools (test environment only). When Marketplace:TestTools is on, GET /api/public/marketplace/test-tools returns enabled: true. The buying organization’s owner or admin can then use two endpoints:
POST /api/org/{slug}/marketplace/apps/{groupKey}/test-clockwithaction:status: show where the subscription stands.advance_month: move the subscription’s Stripe test clock to the next renewal.fail_next: make the next renewal payment fail.succeed_next: make the next renewal payment go through.
GET .../reconcile: compare Stripe’s payments with the payment ledger line by line, and show Axowl’s install state.
Both answer 404 everywhere else.