# Product Feed (ACP)
Source: https://docs.droplinked.com/agentic/acp-feed
The Stripe Agentic Commerce Protocol feed — how a merchant's published inventory becomes shoppable inside AI surfaces.
Droplinked publishes every merchant's public catalog as a **Stripe Agentic Commerce Protocol
(ACP) product feed**. This is the surface that agentic shopping experiences (e.g. ChatGPT)
ingest — so a merchant's existing inventory is discoverable and purchasable by agents without
the merchant building anything new.
## Live feed
Always-current public feed at `https://apiv3.droplinked.com/feed/acp.json`. Cursor-paginated;
serves the full public catalog (thousands of items). Updated on product changes plus a short
refresh cycle for inventory + price.
## Feed shape
```json theme={null}
{
"items": [
{
"id": "…",
"item_group_id": "…",
"title": "…",
"description": "…",
"link": "https://…",
"image_link": "https://…",
"availability": "in_stock",
"price": "300.00 USD",
"condition": "new",
"brand": "…"
}
],
"next_cursor": "…",
"generated_at": "…"
}
```
| Field | Meaning |
| ---------------------------- | ----------------------------------------- |
| `id` / `item_group_id` | Product (variant) and parent grouping ids |
| `title` / `description` | Display copy |
| `link` / `image_link` | Storefront product URL and primary image |
| `availability` / `condition` | Stock + condition (`in_stock`, `new`, …) |
| `price` | Amount + currency |
| `brand` | Merchant/brand |
Paginate by following `next_cursor` until it's absent. `generated_at` reflects the snapshot time.
## Quick agent integration
```bash curl theme={null}
# Browse the live feed (first 3 items)
curl -sS https://apiv3.droplinked.com/feed/acp.json | jq '.items[0:3]'
```
```javascript js theme={null}
// Use in your agent's product-discovery tool
const feed = await fetch('https://apiv3.droplinked.com/feed/acp.json').then(r => r.json());
const matching = feed.items.filter(p => p.title.toLowerCase().includes(userQuery));
return matching.slice(0, 5).map(p => ({
title: p.title, price: p.price, checkout: p.link
}));
```
```python python theme={null}
import requests
feed = requests.get('https://apiv3.droplinked.com/feed/acp.json').json()
# pipe into your agent's tool definitions
for product in feed['items']:
yield {'id': product['id'], 'title': product['title'], 'url': product['link']}
```
## How it's used
* **Agents & shopping surfaces** ingest the feed to make Droplinked inventory searchable and
buyable in-context.
* The [MCP server](/agentic/mcp-server) lets an agent query the same catalog directly via
`searchProducts` / `findInventory` rather than ingesting the whole feed.
* Purchases route through the Stripe ACP / x402 layer with the 70/20/10 revenue split.
## ACP-compatible surfaces
* **Stripe ACP-aware agents** — Droplinked's feed is registered with Stripe's ACP partner catalog
* **OpenAI / ChatGPT shopping surfaces** — feed URL can be added to agent tool-defs as a discovery source
* **Visa Intelligent Commerce** (when GA) — same feed format
## Related
Direct programmatic API access for agents (search, fetch, checkout).
Full machine-readable API surface (cart, checkout, orders) for fresher-than-feed data.
Make your existing inventory agent-shoppable.
## Cache + refresh
* Feed regenerates on product create/edit/delete plus a short refresh cycle for inventory + price
* For agents requiring up-to-the-second data, hit the cart/checkout endpoints in the [OpenAPI Spec](/api-reference/openapi-spec) directly
# AEO/GEO Optimization
Source: https://docs.droplinked.com/agentic/aeo-geo-optimization
Make your catalog rank well in answer engines (ChatGPT, Perplexity, Google AI Overviews) and generative engines (Claude, Gemini, agent shopping surfaces).
Traditional SEO optimizes for ranked link results. **AEO** (Answer Engine
Optimization) and **GEO** (Generative Engine Optimization) optimize for the
new surface area — LLM-driven search and agentic shopping where the engine
synthesizes an answer instead of returning a list. Your product either gets
cited or it doesn't.
This guide is for merchants who have already
[opted into MCP discovery](/agentic/get-listed-in-mcp-discovery). The same
catalog metadata that drives MCP ranking also drives AEO/GEO visibility.
## AEO vs GEO — the short version
* **AEO** — optimize for engines that synthesize a single answer from
retrieval (ChatGPT search, Perplexity, Google AI Overviews, Bing Copilot).
These engines need clear, structured facts they can quote.
* **GEO** — optimize for generative engines that compose multi-tool agentic
responses (Claude with tool use, Gemini agentic shopping, MCP-backed
shopping agents). These engines need machine-readable surface area they
can pivot through.
The two overlap heavily. Most of the work helps both.
## The four levers
Titles + descriptions that read like facts, not marketing.
Schema.org Product markup + Open Graph + JSON-LD so engines parse you
deterministically.
Surface common buyer questions in copy that answer engines can extract
verbatim.
One stable URL per product, kept current. Stale data is the #1 cause of
de-ranking.
## Product titles
Answer engines and shopping agents both rank on title clarity. The pattern
that wins:
```
[Brand] [Product type] — [defining attribute] [secondary attribute] [size/variant]
```
| Avoid | Prefer |
| ---------------------------------- | ------------------------------------------------------------------------------------ |
| `Cozy AF Beanie 🔥` | `WarmWool Classic Beanie — 100% merino wool, ribbed knit, one size` |
| `The Best Coffee You'll Ever Have` | `BlueOak Single-Origin Coffee — Ethiopian Yirgacheffe, light roast, 12oz whole bean` |
| `New Drop!!! Limited Edition!!!` | `Atlas Carry-On Backpack — 30L, water-resistant nylon, fits 16" laptop` |
The agent is going to surface this title to a buyer in a sentence. It needs
to read as a fact.
## Product descriptions
Three-paragraph pattern that maps well to both retrieval and agentic synthesis:
1. **First sentence** — declarative summary of what the product is and who
it's for. The engine will quote this verbatim in answers.
2. **Body** — material, dimensions, included items, key specs. Bullet lists
are easier for retrieval to chunk than prose.
3. **Closing** — care instructions, sizing notes, FAQ-style edge cases.
Avoid:
* Repeating the title in the first sentence
* Marketing fluff above the fold ("This is more than a beanie — it's a
statement")
* Specs hidden in image alt text only
## Schema.org Product markup
The most impactful structured-data investment. Embed JSON-LD in the storefront
`
` of every product page:
```html theme={null}
```
For storefronts hosted on `droplinked.io`, the template injects this
automatically. For Shopify and custom storefronts, add it to your product
template.
## Open Graph + Twitter cards
Shareable links that appear correctly in chat surfaces (Slack, iMessage, X,
Discord) are far more likely to get cited by an agent crawling that channel.
```html theme={null}
```
## FAQ blocks
Answer engines aggressively extract FAQ blocks. Add a short FAQ section to
high-traffic product pages and mark it up:
```html theme={null}
```
## Canonical URLs + freshness
* **One canonical URL per product** — pass `` on every
variant page that points back to the parent product.
* **Don't break URLs on rename** — if you rename the product, 301-redirect
the old slug.
* **Keep stock + price accurate** — engines de-rank stale catalog data. The
Droplinked ACP feed refreshes on every product write plus a short
background cycle.
## Storefront-level signals
Beyond per-product metadata, engines weight storefront-level signals:
* **`robots.txt`** allowing `User-agent: *` (or explicitly `GPTBot`,
`ClaudeBot`, `PerplexityBot`, `Google-Extended`)
* **A sitemap** at `/sitemap.xml` listing every product URL
* **Stable storefront name** — frequent rebranding fragments your signal
* **MCP discovery meta tag** in `` (see
[Storefront MCP Discovery](/agentic/storefront-mcp-discovery))
## Verification
Validate your structured data:
* [Google Rich Results Test](https://search.google.com/test/rich-results) —
Product + FAQPage support
* [Schema.org Validator](https://validator.schema.org/) — generic JSON-LD
validation
* [OpenGraph.xyz](https://www.opengraph.xyz/) — preview Open Graph cards
Verify your product is in the agentic feed:
```bash theme={null}
curl -s "https://apiv3.droplinked.com/feed/acp.json" \
| jq '.items[] | select(.title | contains("Classic Beanie"))'
```
## Anti-patterns
* **Stuffing brand into every field** — the engine treats this as noise.
* **Hiding specs in images** — alt text helps but is not a substitute for
body copy.
* **Identical descriptions across variants** — variants should share the
parent description and add only their differentiator.
* **Auto-translated copy without review** — synthesized translations often
fail AEO ranking because key terms get paraphrased.
* **Blocking AI crawlers in `robots.txt`** — if you want agentic
discovery, you can't block the agents.
## Next steps
The prerequisite — your shop has to be projecting to the ACP feed and
MCP server first.
See how a well-ranked catalog converts to USDC + fiat revenue.
# Attribution + Commission
Source: https://docs.droplinked.com/agentic/attribution-and-commission
How agentic conversions are attributed to the AI surface that drove them, and how the 70/20/10 split settles in USDC + fiat.
When an AI agent surfaces a Droplinked product to a buyer and the buyer
converts, three parties earn:
| Party | Share | Why |
| ---------------------- | ----- | --------------------------------------------------------------------------------- |
| **Merchant** | 70% | The product, the inventory, the fulfillment |
| **Droplinked** | 20% | Discovery surface (ACP feed + MCP server), settlement, attribution infrastructure |
| **AI-agent affiliate** | 10% | The agent that surfaced the product to the buyer |
This page walks through how that attribution actually happens — for merchants,
agent developers, and anyone integrating against the
[MCP server](/agentic/mcp-server).
## Two attribution paths
Droplinked attributes agentic conversions on two distinct rails, depending on
whose catalog the agent is shopping:
| Path | Catalog | How intent is captured | How a conversion is recorded |
| ------------ | -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Internal** | A Droplinked merchant's own catalog (native `ProductV2`) | `recordAgenticIntent` writes a row to the agentic intent ledger (or `placeOrderAgentic` orders directly) | On the order-confirmed event, the settlement coordinator marks the intent converted, computes commission, and writes a settlement-ready accrual |
| **External** | A third-party advertiser in the Flatlay × Impact network, surfaced by `findInventory` (`source: impact_brand`) | An **external** intent row is written (no Droplinked shop / `merchantId: null`, `external: true`) capturing the advertiser + product + Model-A tracked link; the agent drives the buyer to the `trackedBuyUrl` (`subId1=dl-agentic:{intentId}`) | Droplinked **reconciles** the sale by reading Impact's `/Actions` endpoint and matching `SubId1` back to the intent row — no Droplinked order exists |
Both paths key the conversion to the same `intentId`. They differ in **where the buyer
converts** and therefore **how the sale comes back**:
* The **internal** path (behind `AGENTIC_DISCOVERY_SETTLEMENT_ENABLED`) is detailed in
[the next section](#the-internal-intent-ledger). The buyer converts on a Droplinked order, so
the settlement coordinator owns the whole loop.
* The **external** path (behind `IMPACT_ATTRIBUTION_ENABLED`) is detailed in
[External (Impact advertiser) path](#external-impact-advertiser-path). There is **no
Droplinked order** — the buyer converts on the advertiser's own site — so Droplinked records
the attribution intent up front and reconciles the real commission by **reading Impact's
`/Actions` report**, rather than minting an order.
## The internal intent ledger
For Droplinked-merchant catalogs, attribution is **explicit and idempotent**:
The agent calls `recordAgenticIntent` with the merchant slug and an intent
of `browse` / `buy` / `compare` / `share`. The merchant must have opted
into agentic discovery (`agenticDiscoveryEnabled`) — otherwise the call
returns `opt_in_required` and no row is written. An optional caller-supplied
`intentId` makes retries idempotent, and an optional `referringAffiliateId`
(a Mongo ObjectId) tags the affiliate of record.
The agent calls `placeOrderAgentic` (or the buyer completes a checkout link
that carries the `intentId`). The order is created and follows the normal
PSP-resolver lifecycle.
On the order-confirmed event, the settlement coordinator marks the matching
intent row converted, computes the commission, and writes a settlement-ready
accrual — freezing the affiliate-of-record at that moment. The whole path is
idempotent end-to-end (it no-ops cleanly for orders that didn't originate
from an MCP intent).
## External (Impact advertiser) path
When the agent shops a third-party advertiser's catalog surfaced by `findInventory`
(`source: impact_brand`), there is **no Droplinked shop and no Droplinked order**. Attribution
rides Model A — a tracked deep-link the advertiser's pixel credits — and Droplinked owns the
attribution record + reconciliation (it does not just hand off to Impact):
`findInventory` (with `IMPACT_LIVE_SEARCH_ENABLED` on) returns live `impact_brand` results
from Impact's multi-brand catalog. An **external** intent row is written to the ledger with
`merchantId: null`, `external: true`, and the advertiser / product / tracked-link capture —
so the row is self-describing for reconciliation and audit.
When an `intentId` is passed and `IMPACT_ATTRIBUTION_ENABLED` is on, each `impact_brand`
item carries a `trackedBuyUrl` tagged `subId1=dl-agentic:{intentId}`. The agent drives the
buyer to that deep-link; the buyer checks out **on the advertiser's site**.
A daily reconciliation job (and an admin-triggerable endpoint) reads Impact's `/Actions`
report over a sliding window, parses `SubId1` back to the `intentId`, and records the real
commission (`payoutUsd`), sale amount, and state (`PENDING` / `APPROVED` / `REVERSED`)
against the matching intent. Idempotent on Impact's order id (`oid`), so re-running over an
overlapping window updates the same record in place.
The external path is **fail-open and gated**: with `IMPACT_ATTRIBUTION_ENABLED` off (or the
read-write publisher credentials absent), no tracked link is composed, no `/Actions` read
happens, and `findInventory` simply returns products without a `trackedBuyUrl`. It never
blocks discovery.
Some MCP clients connect through a hosted gateway that signs every call with
an API key and derives attribution from the signed tool chain. The
description below covers that gateway model. The backend's own canonical
attribution mechanism is the explicit intent ledger described above — when in
doubt, call `recordAgenticIntent`.
## How attribution works (gateway model)
Through the hosted gateway, attribution is **tool-call-driven**, not
click-driven. There's no cookie, no UTM parameter, no redirect chain. The flow:
Every call to `mcp.droplinked.com/mcp/tools/{name}` is signed with the
agent's `X-MCP-API-Key`. The MCP server records the call chain (search →
detail → cart → order) against that key with a correlation id.
When the agent calls `placeOrderAgentic`, the resulting order is tagged
with the agent's identity (via the API key) at creation time. This tag
persists on the order document and follows it through the lifecycle.
The PSP captures payment via the resolver chain (`creditCard` / `paypal`
/ `crypto` / `wallet`, picked from the merchant's authorized pool — see
[PSP-invisible to customer/agent](/concepts/platform-model)). Capture is
the attribution-eligibility moment — refunds reverse it.
On capture, the settlement engine reads the agent tag and the merchant's
payout preferences, then routes the three shares.
The agent does **not** need to call a separate "record intent" tool —
attribution is implicit in the API-key-signed tool chain.
## Idempotency + edge cases
* **Multiple agents in a chain** — if Agent A calls `searchProducts` and
Agent B calls `placeOrderAgentic` on the same cart, attribution goes to
Agent B (the order-placer).
* **Human-completed checkout** — if the agent only returns a checkout link
to the buyer (instead of calling `placeOrderAgentic`), attribution still
holds via the `intentId` embedded in the checkout URL.
* **Refunds** — refunding an order via `refundOrder` (or any other refund
channel) reverses the commission. The agent's share is clawed back from
pending or future settlements.
* **Disputes** — chargeback losses reverse the commission for the merchant
* agent. Droplinked's 20% covers dispute infrastructure costs and is not
reversed.
## Commission settlement
You can take your share on either of two rails. Most merchants and agents
take both.
### USDC on Base via x402
* **Cadence** — continuous. Settles per-order as soon as the payment
capture clears.
* **Pros** — fast, low-fee, programmable. Suitable for high-volume agent
traffic where waiting weekly would tie up capital.
* **Onboard** — connect a Base-compatible wallet in
**Dashboard → Wallet → Connect Base**. USDC streams to the connected
address.
### Fiat off-ramp via Stripe
* **Cadence** — weekly. Drops on the merchant's Stripe payout schedule
(default: every Monday).
* **Pros** — bank-account-native, integrates with existing accounting
flows.
* **Onboard** — your existing Stripe-connected account, no extra setup.
Off-ramp uses Stripe's standard payout schedule.
## Dashboard visibility
Merchants and agent developers each see attribution + commission state in
their dashboards:
* **Merchants** — `/management/billing-history` shows order-by-order
commission deductions, the agent that drove each order (when applicable),
and the rolling payout in both rails.
* **Agent developers** — `/management/agent-earnings` shows per-key tool
call counts, conversion attribution, and the rolling commission balance
in both rails.
A small percentage of orders will surface as **unattributed agentic** in
the dashboard — these are agentic shopping sessions Droplinked detected
(e.g. user-agent strings matching known AI clients) where no MCP key was
present on the call. Droplinked still recognizes these orders as agentic
for reporting but does not pay the 10% affiliate share for them.
## Refund + dispute mechanics
| Event | Merchant share | Droplinked share | Agent share |
| ----------------- | -------------- | ---------------- | --------------- |
| Order captures | +70% | +20% | +10% |
| Refund | -70% | -20% | -10% |
| Chargeback (lost) | -100% (lost) | -20% (clawback) | -10% (clawback) |
| Chargeback (won) | no change | no change | no change |
The agent share is always offset against future settlements — there's no
clawback from a wallet, just a deduction from the rolling balance.
## Anti-fraud
Droplinked monitors tool-call patterns for attribution gaming:
* **Self-attribution** — an agent calling tools against a shop it operates.
Detected via merchant ↔ MCP key linkage; commission clawed back.
* **Click-farming** — high tool-call volume with near-zero conversion.
Triggers a manual review; can result in key suspension.
* **Wash trading** — the same buyer email + payment method recurring across
many small orders. Captured via the order graph.
These checks run async — they don't slow live settlement, but suspicious
balances are held for 14 days before payout.
## Verification
Confirm attribution on a known order:
```bash theme={null}
curl -s -X POST https://mcp.droplinked.com/mcp/tools/getOrderReceipt \
-H "X-MCP-API-Key: $DROPLINKED_MCP_KEY" \
-H "content-type: application/json" \
-d '{"orderId": "ord_…"}' \
| jq '{orderId, attributedAgentKey, paymentMethod, totals}'
```
The `attributedAgentKey` field will match your key prefix if attribution
landed correctly. (Only the prefix is exposed for privacy.)
## Next steps
Merchant-facing: opt your catalog into the discovery surface that drives
these commissions.
Agent-developer-facing: how to call the tool surface that drives
attribution.
# Cart Composition for Agents
Source: https://docs.droplinked.com/agentic/cart-composition
Build a multi-line Droplinked cart across turns using `start_checkout` plus the four cart-mutation tools (cart.addLine, cart.updateLineQuantity, cart.removeLine, cart.applyDiscount).
`start_checkout` was the original "find ⇨ buy" path: an agent calls it once with a SKU,
gets back a hosted Droplinked checkout URL, and hands the buyer off to pay. That works for
single-SKU intent ("buy this thing") but not for **multi-turn agentic composition**, where
the agent (or buyer) wants to *try* a cart configuration before paying.
The four **cart-mutation tools** close that loop:
| Tool | What it does |
| ------------------------- | ------------------------------------------------ |
| `cart.addLine` | Add another SKU to an existing cart |
| `cart.updateLineQuantity` | Change the quantity of a SKU already in the cart |
| `cart.removeLine` | Remove a SKU from the cart |
| `cart.applyDiscount` | Apply a merchant-issued discount / coupon code |
All four expect the **encrypted `cartId`** returned by `start_checkout` — pass it back
verbatim; it's already obfuscated by the backend's `DecryptCartIdPipe`.
***
## Canonical multi-line composition flow
```text theme={null}
1. search_products({ query: "blue sneaker size 10" }) → [skuA, skuB, …]
2. start_checkout({ shopId, skuId: skuA, quantity: 1, … }) → { cartId, checkoutUrl }
3. cart.addLine({ cartId, skuId: skuB, quantity: 2 }) → updated cart
4. cart.updateLineQuantity({ cartId, skuId: skuA, quantity: 3 })
→ bumped cart total
5. cart.applyDiscount({ cartId, discountCode: "WELCOME10" }) → discount applied
6. Buyer (or agent) navigates to `checkoutUrl` and pays
```
Each step returns the updated `cart` envelope so the agent can show the running subtotal,
line list, and discount before commit. There is no separate "preview" call — every mutation
is the preview.
***
## Tool reference
### `cart.addLine`
```json theme={null}
{
"cartId": "ENC:...",
"skuId": "65ab12cd34ef567890123456",
"quantity": 2
}
```
Idempotent on (`cartId`, `skuId`): calling twice with the same SKU bumps the quantity on
the existing line rather than creating a duplicate.
**Backs**: `POST /v2/carts/:cartId/products`
### `cart.updateLineQuantity`
```json theme={null}
{
"cartId": "ENC:...",
"skuId": "65ab12cd34ef567890123456",
"quantity": 5
}
```
Sets the line to the specified quantity. **Passing `quantity: 0` removes the line** — same
effect as calling `cart.removeLine` but in a single call, which is convenient when the
agent is decrementing.
**Backs**: `PATCH /v2/carts/:cartId/products/:skuId`
### `cart.removeLine`
```json theme={null}
{
"cartId": "ENC:...",
"skuId": "65ab12cd34ef567890123456"
}
```
Removes the line entirely. The returned cart shows the updated lines + total. Removing the
last line leaves the cart empty but still usable — the buyer can continue browsing and the
agent can add new lines.
**Backs**: `DELETE /v2/carts/:cartId/products/:skuId`
### `cart.applyDiscount`
```json theme={null}
{
"cartId": "ENC:...",
"discountCode": "SAVE20"
}
```
Validates the discount code at the merchant level, computes the discount, and returns the
updated cart with the discount amount + label resolved. The code is case-insensitive on
the backend but is preserved on the MCP surface so agents can run idempotency checks
against the exact value they sent.
An invalid or expired code surfaces as a structured error envelope.
**Backs**: `POST /v2/carts/:cartId/coupon`
***
## Error envelope
All four tools follow the same envelope shape so agents can branch on JSON instead of
catching exceptions:
```json theme={null}
// Happy path
{ "status": "ok", "cartId": "ENC:...", "...": "..." }
// Failure (backend error, validation error, network error)
{ "status": "error", "reason": "", "message": "…human-readable…" }
```
The structured `reason` enum lets agents branch programmatically (e.g.
`reason: "INVALID_COUPON"` vs `reason: "BACKEND_5XX"`).
***
## When NOT to use the cart-mutation tools
* **Agent only knows the buyer wants one SKU**: use `start_checkout` alone. The mutation
tools are pure overhead for single-SKU intent.
* **Agent is composing for an *unauthenticated* buyer**: there is no "guest cart" — the
cart belongs to the email passed to `start_checkout`. Mutations apply to that cart.
* **Agent wants to express discount programmatically**: the only discount surface is the
discount code. There is no `cart.applyPercentageOff` or similar — merchants own their
promo logic via discount codes.
***
## Related
* [Inventory MCP](/agentic/inventory-mcp) — full tool inventory
* [MCP Server](/agentic/mcp-server) — installation and environment reference
* [Storefront MCP discovery](/agentic/storefront-mcp-discovery) — the `search_products`
/ `get_product` family that precedes `start_checkout`
* [ACP feed](/agentic/acp-feed) — the bulk-discovery feed agents can crawl before
cart-composing
# Connect WooCommerce, BigCommerce, Magento, or custom
Source: https://docs.droplinked.com/agentic/connect-other-platforms
Plug WooCommerce, BigCommerce, Magento 2.x, or a custom storefront into droplinked's InventoryOS. Same pattern as the Shopify connector — webhook receiver, HMAC validation, one-way mirror.
The [Shopify connector pattern](/agentic/connect-shopify) generalizes. Any platform that can emit webhooks on catalog + order events — WooCommerce, BigCommerce, Magento, or a custom Next.js / React / Express / Rails / Django storefront — can plug into [InventoryOS](/concepts/inventory-os) using the same webhook-receiver + signing-validation shape.
This page walks through the per-platform webhook setup for the four most-requested origin platforms beyond Shopify. The droplinked side is identical in each case: one normalized catalog mirror, one per-merchant MCP surface, one ACP feed entry, one lender-routing eligibility.
## Platform-specific integration
Configure webhooks from **WP Admin → WooCommerce → Settings → Advanced → Webhooks**. Point each event at the droplinked WooCommerce ingestion endpoint and copy the shared HMAC secret you generated in Step 1 below.
| WooCommerce topic | Destination |
| ----------------- | ------------------------------------------------------------------ |
| `product.created` | `https://apiv3.droplinked.com/v2/integrations/woo/webhook/product` |
| `product.updated` | `https://apiv3.droplinked.com/v2/integrations/woo/webhook/product` |
| `product.deleted` | `https://apiv3.droplinked.com/v2/integrations/woo/webhook/product` |
| `order.created` | `https://apiv3.droplinked.com/v2/integrations/woo/webhook/order` |
| `order.updated` | `https://apiv3.droplinked.com/v2/integrations/woo/webhook/order` |
WooCommerce uses a webhook secret in the URL signing scheme — droplinked's WooCommerce adapter does **HMAC-SHA256 validation parity** on the raw body using the secret you provided at connect time. Format: **JSON**.
A dedicated WordPress plugin for one-click install is on the [InventoryOS roadmap](/concepts/inventory-os#whats-coming). Until then, the WP Admin webhook config above is the supported integration path.
Configure webhooks from the **BigCommerce Control Panel → Apps → Webhooks** (or programmatically via the V3 Webhooks API). Subscribe to these scopes pointed at the droplinked BigCommerce ingestion endpoint:
| BigCommerce scope | Destination |
| ----------------------- | ------------------------------------------------------------------ |
| `store/product/created` | `https://apiv3.droplinked.com/v2/integrations/bigcommerce/webhook` |
| `store/product/updated` | `https://apiv3.droplinked.com/v2/integrations/bigcommerce/webhook` |
| `store/product/deleted` | `https://apiv3.droplinked.com/v2/integrations/bigcommerce/webhook` |
| `store/order/created` | `https://apiv3.droplinked.com/v2/integrations/bigcommerce/webhook` |
| `store/order/updated` | `https://apiv3.droplinked.com/v2/integrations/bigcommerce/webhook` |
BigCommerce delivers webhooks with a **signed JWT payload** rather than an HMAC header. Droplinked's BigCommerce adapter validates the JWT signature against the per-merchant signing key configured at connect time.
An official BigCommerce App on the App Marketplace is on the [InventoryOS roadmap](/concepts/inventory-os#whats-coming). Until then, configuring webhooks directly via the Control Panel (or the V3 Webhooks API) is the supported integration path.
Configure webhooks from **Admin → Stores → Configuration → Services → Webhooks** (Magento 2.4+ exposes a native Webhooks surface; older versions can install the Magento Webhooks extension). Subscribe to these events pointed at the droplinked Magento ingestion endpoint:
| Magento event | Destination |
| ------------------------------ | -------------------------------------------------------------- |
| `catalog_product_save_after` | `https://apiv3.droplinked.com/v2/integrations/magento/webhook` |
| `catalog_product_delete_after` | `https://apiv3.droplinked.com/v2/integrations/magento/webhook` |
| `sales_order_save_after` | `https://apiv3.droplinked.com/v2/integrations/magento/webhook` |
Magento uses **RSA-signature signing** on extension-emitted webhooks. Droplinked's Magento adapter validates the RSA signature against the per-merchant public key registered at connect time.
A Magento 2 extension (Adobe Commerce Marketplace listing) is on the [InventoryOS roadmap](/concepts/inventory-os#whats-coming). Until then, the Magento Webhooks surface (or extension) above is the supported integration path.
For a custom Next.js, React, Express, Rails, Django, or any other headless storefront, emit JSON webhooks directly to droplinked's custom ingestion endpoint on product + order events:
```bash theme={null}
curl -X POST 'https://apiv3.droplinked.com/v2/integrations/custom/webhook' \
-H 'content-type: application/json' \
-H 'X-Droplinked-Shop-Slug: your-shop-slug' \
-H "X-Droplinked-Hmac-Sha256: $(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$HMAC_SECRET" -binary | base64)" \
-d "$BODY"
```
Where `$BODY` is your JSON payload — for example, on a product create:
```json theme={null}
{
"event": "product.created",
"occurredAt": "2026-06-13T12:00:00Z",
"product": {
"originId": "sku_42",
"title": "Carbon Hoodie",
"description": "…",
"variants": [
{ "originVariantId": "sku_42_m", "size": "M", "price": { "currency": "USD", "amount": 4900 } }
],
"images": ["https://cdn.example.com/sku_42.jpg"],
"vendor": "Your Brand",
"tags": ["hoodie", "carbon"]
}
}
```
Droplinked validates the `X-Droplinked-Hmac-Sha256` header against the HMAC secret you generated at connect time (SHA256 over the raw request body, base64-encoded).
An SDK-style helper for the most common runtimes is coming as `@droplinked/connector-sdk` — see the [InventoryOS roadmap](/concepts/inventory-os#whats-coming).
## What you need before connecting
Regardless of platform, the connect handshake gives you the three things every webhook receiver needs:
* **Your droplinked merchant ID** — assigned at operator-onboarding time (`mch_…`)
* **A shared HMAC secret** — generated when you call `POST /admin/integrations/connect` with your origin platform domain + preferred shop-slug
* **Origin-platform webhook configuration access** — admin rights on the origin platform to wire up the webhook endpoints above
Once you have all three, the platform-specific tab above tells you exactly which events to subscribe to.
## Validation pattern
Droplinked validates each origin platform's webhooks using the signing scheme that platform emits — the validation logic is per-adapter but the security posture is uniform:
| Platform | Signing scheme | Validation |
| ----------------------------------- | ------------------------- | -------------------------------------------------------------------- |
| [Shopify](/agentic/connect-shopify) | SHA256 HMAC over raw body | `X-Shopify-Hmac-Sha256` header vs computed HMAC |
| WooCommerce | SHA256 HMAC parity | `X-WC-Webhook-Signature` header vs computed HMAC |
| BigCommerce | Signed JWT payload | JWT signature verified against per-merchant signing key |
| Magento | RSA signature | Extension-emitted signature verified against per-merchant public key |
| Custom storefront | SHA256 HMAC over raw body | `X-Droplinked-Hmac-Sha256` header vs computed HMAC |
A failed signature check rejects the webhook with `401 Unauthorized` — no partial state is written into [InventoryOS](/concepts/inventory-os).
## What you gain — same as Shopify
The droplinked-side surfaces every connector unlocks are identical to what [Shopify merchants](/agentic/connect-shopify) get:
* **Agent-shoppable distribution** via your per-merchant MCP at `mcp.droplinked.com/{shop-slug}/...`
* **ACP feed inclusion** at `apiv3.droplinked.com/feed/acp.json`
* **Lender-routing eligibility** via the [LenderRegistry](/api-reference/public/lender-registry) + [recommendation endpoint](/api-reference/public/lender-routing)
* **Brand attestation** (Schema A) — operator-gated, makes your brand cryptographically verifiable to agents and verifiers
* **Built-in affiliate network** with x402 settlement on the 70/20/10 split
* **Cross-channel reconciliation** if you connect more than one origin platform (see below)
## Cross-channel posture
A merchant connecting both Shopify **and** WooCommerce — or BigCommerce + a custom Next.js site, or any other combination — gets a **unified InventoryOS view**: one normalized catalog, one repayment-history rollup, one lender-routing decision, one per-merchant MCP. The brand attestation is issued once and applies across every connected channel.
See [InventoryOS — cross-channel reconciliation](/concepts/inventory-os#cross-channel-reconciliation) for the architectural deep-dive on why we bridge channels rather than silo them.
## Related
* [Connect your Shopify store](/agentic/connect-shopify) — the live, fully-built reference adapter
* [InventoryOS](/concepts/inventory-os) — the bridge layer every connector feeds into
* [Trust fabric](/concepts/trust-fabric) — Schema A/B/C/D attestation architecture
* [For merchants](/concepts/for-merchants) — how droplinked composes with your existing commerce stack
# Connect your Shopify store
Source: https://docs.droplinked.com/agentic/connect-shopify
Plug your existing Shopify catalog into droplinked's InventoryOS — agent-shoppable distribution, lender-ecosystem financing, on-chain trust-fabric posture — without re-platforming or migrating. Keep your Shopify storefront, checkout, and admin.
Droplinked doesn't replace Shopify. It plugs into your existing store as an **additive layer**
— the same way an analytics suite, a review widget, or a loyalty app does — except the
surface it adds is agentic distribution, cross-channel inventory, and access to a
licensed-lender financing ecosystem.
What you keep: your Shopify storefront, theme, checkout flow, admin, the entire Shopify app
ecosystem, and your Shopify Payments routing. What you gain: an InventoryOS mirror that
keeps your catalog reconciled across channels, agent-shoppable distribution via a
per-merchant MCP surface plus the ACP feed, and access to droplinked's LenderRegistry for
credit lines beyond Stripe Capital's balance-sheet ceiling.
No re-platforming. No catalog migration. No customer-data move. Connect, sync, and route.
## What you gain
ChatGPT, Claude, Cursor, and the OpenAI Agents SDK discover your catalog through a
per-merchant MCP surface and the ACP feed.
InventoryOS reconciles stock across Shopify, droplinked, and future channels — one
canonical view, Shopify stays source of truth.
Beyond Stripe Capital: licensed DeFi lenders and treasuries, methodology-pinned credit
attestations, jurisdiction-aware routing.
A Schema A brand attestation makes your brand cryptographically verifiable to agents and
verifiers — impostors can't claim your slug.
Beyond Shopify Payments — Stripe + PayPal + Telr + Bonum + PayMob, corridor-aware
routing for the regions Shopify Payments doesn't cover.
Droplinked's affiliate program runs alongside Shopify — no app install, 70/20/10 split
on conversions sourced by agents and publishers.
## What stays in Shopify
You keep, untouched:
* Your storefront URL, theme, and brand chrome
* Shopify checkout (or whatever custom checkout you already run)
* Shopify customer accounts and order history
* Shopify Payments and your existing payout schedule
* Shopify admin, reports, and analytics
* Every Shopify app and plugin you've already installed
## What gets added on droplinked
After you connect, droplinked maintains:
* A read-only mirror of your catalog inside droplinked InventoryOS
* A per-merchant MCP surface at `mcp.droplinked.com/{shop-slug}/...` (live today)
* Inclusion in the ACP feed at `apiv3.droplinked.com/feed/acp.json`
* A Schema A brand attestation (operator-gated — see below)
* Lender-routing recommendations via `GET /v2/lender-routing/recommend`
* Optional affiliate-network participation with x402 settlement
Shopify Admin → Settings → Apps doesn't change. The droplinked surface lives at
`droplinked.com` (merchant dashboard) and exposes the per-merchant MCP at
`mcp.droplinked.com/{shop-slug}` — completely outside the Shopify control plane.
## The 5-step integration
The dedicated Shopify App Store listing is planned (see [What's coming](#whats-coming))
— until then, connect via Shopify Admin's built-in webhook configuration. This pattern
keeps your data plane simple and avoids an app-install permission grant beyond what
droplinked needs to mirror your catalog.
In Shopify Admin → **Settings** → **Notifications** → **Webhooks**, add these events
pointed at the droplinked ingestion endpoint:
| Shopify event | Destination |
| ------------------------- | -------------------------------------------------------------- |
| `products/create` | `https://apiv3.droplinked.com/v2/integrations/shopify/webhook` |
| `products/update` | `https://apiv3.droplinked.com/v2/integrations/shopify/webhook` |
| `inventory_levels/update` | `https://apiv3.droplinked.com/v2/integrations/shopify/webhook` |
| `orders/create` | `https://apiv3.droplinked.com/v2/integrations/shopify/webhook` |
| `orders/updated` | `https://apiv3.droplinked.com/v2/integrations/shopify/webhook` |
Format: **JSON**. Shopify will sign each request with HMAC-SHA256 using the secret it
displays at webhook-creation time — keep that value, you'll hand it to droplinked in
Step 2.
Hand droplinked the Shopify shop domain and the webhook HMAC secret. Droplinked
validates every inbound webhook signature, creates the droplinked merchant entity, and
binds the shop-slug routing for steps 3-5.
```bash theme={null}
curl -X POST 'https://apiv3.droplinked.com/v2/integrations/shopify/connect' \
-H 'authorization: Bearer YOUR_DROPLINKED_API_KEY' \
-H 'content-type: application/json' \
-d '{
"shopifyShopDomain": "your-store.myshopify.com",
"shopifyWebhookSecret": "shpss_…",
"preferredShopSlug": "your-shop-slug"
}'
```
Response includes the assigned `droplinkedMerchantId` and the final `shopSlug` (which
becomes part of your per-merchant MCP URL).
Treat `shopifyWebhookSecret` as a credential. Pass it over TLS, never check it into
source control, and rotate it if it leaks — Shopify will let you regenerate it from
the same Notifications panel.
Webhooks only carry products from the moment they're installed forward. To backfill
your existing catalog into droplinked InventoryOS, kick a bulk sync:
```bash theme={null}
curl -X POST 'https://apiv3.droplinked.com/v2/integrations/shopify/sync' \
-H 'authorization: Bearer YOUR_DROPLINKED_API_KEY' \
-H 'content-type: application/json' \
-d '{ "droplinkedMerchantId": "mch_…" }'
```
Droplinked pages through the Shopify Admin GraphQL API (`products` connection) using
your stored credentials and writes a read-only mirror into InventoryOS. Typical
catalogs (1k-10k SKUs) finish in minutes; large catalogs run async and emit a webhook
when complete.
Verify your catalog is now reachable through the per-merchant MCP surface and the ACP
feed.
Per-merchant MCP manifest:
```bash theme={null}
curl https://mcp.droplinked.com/your-shop-slug/manifest
```
Per-merchant tool list (search, get product, start checkout):
```bash theme={null}
curl https://mcp.droplinked.com/your-shop-slug/mcp/tools
```
Your products in the ACP feed:
```bash theme={null}
curl -s https://apiv3.droplinked.com/feed/acp.json \
| jq '.items[] | select(.brand == "Your Brand Name")'
```
See [Storefront MCP Discovery](/agentic/storefront-mcp-discovery) for the one-line
Shopify Liquid snippet that advertises this MCP URL from your storefront's `` so
agents that land directly on your shop can auto-discover it.
With your catalog mirrored and your shop bound to a droplinked merchant, you're
eligible to apply for financing via the LenderRegistry.
```bash theme={null}
curl -X POST 'https://apiv3.droplinked.com/v2/lending-applications' \
-H 'authorization: Bearer YOUR_DROPLINKED_API_KEY' \
-H 'content-type: application/json' \
-d '{
"droplinkedMerchantId": "mch_…",
"jurisdiction": "AE",
"requestedAmount": { "currency": "USD", "amount": 50000 }
}'
```
The routing engine picks a recommended lender by your jurisdiction and methodology
fit. Approval is operator-gated (per the LenderRegistry trinity) — once issued, your
Schema B credit-risk attestation is on-chain and your repayment history (Schema C)
accrues from Shopify + droplinked orders combined.
## The InventoryOS bridge
InventoryOS is droplinked's cross-channel inventory layer. When you connect Shopify, the
bridge between Shopify and InventoryOS runs by a few simple rules:
* **Shopify is source of truth for stock.** Droplinked maintains a mirror; we never
authoritatively claim a unit you didn't tell us about.
* **Order events from Shopify decrement droplinked's view.** A `orders/create` webhook
updates the mirrored stock level — the next agent query sees the same number Shopify
would have returned.
* **Catalog sync is one-way by default.** Droplinked never overwrites your Shopify catalog
— title, price, description, images all flow Shopify → droplinked, not the other way.
Two-way sync is opt-in and scoped per-field.
* **Conflict resolution.** When the same product attribute is set in both systems, Shopify
wins on price / title / description / images. Droplinked annotates the mirrored record
with attestation metadata (Schema A brand link, Schema C repayment history) the agents
consume.
* **Read more.** A dedicated `/concepts/inventory-os` deep-dive is coming. Until then, the
[platform model](/concepts/platform-model) and [for-merchants concept
page](/concepts/for-merchants) describe how the inventory layer fits the rest of the
stack.
## Agentic distribution flow
How an agent goes from your Shopify storefront URL to a checked-out order:
```mermaid theme={null}
flowchart TD
A[Shopify catalog] --> B[droplinked InventoryOS]
B --> C[Per-merchant MCP surface mcp.droplinked.com/your-shop-slug]
C --> D[ChatGPT / Claude / Cursor / OpenAI Agents SDK]
D --> E[Discover + recommend your product]
E --> F[Buyer routes back to your Shopify checkout]
F --> G[Shopify orders/create webhook to droplinked]
G --> H[Affiliate attribution + Schema C repayment-history attestation]
```
The point: **agents discover and route; Shopify still owns the transaction.** The buyer
checks out on your storefront, against your Shopify Payments, with your tax and shipping
rules. Droplinked only sees the order event after Shopify writes it.
## Optimal financing — the lender-routing pitch
Stripe Capital is excellent — for merchants Stripe wants to fund off its own balance sheet.
Outside that envelope (different jurisdiction, different methodology fit, larger ticket,
faster turnaround), you're on your own.
Droplinked's LenderRegistry surfaces an ecosystem of licensed lenders and DeFi treasuries
(CredibleX in the UAE, Valinor Vault as a global DeFi vault, more in onboarding) and routes
you to the right one based on:
* **Jurisdiction.** `GET /v2/lender-routing/recommend?jurisdiction=AE` returns
exact-jurisdiction matches first, then `GLOBAL` fallbacks, sorted by track record.
* **Methodology fit.** Your underwriting methodology is pinned in a Schema B credit-risk
attestation — verifiers and downstream lenders can verify it cryptographically.
* **Repayment history.** Schema C attestations accrue across Shopify orders + droplinked
orders + any other channel you wire in. Track record stays with you, not with a single
PSP.
* **Tier upgrades.** Use [`/v2/upgrade-preview`](/api-reference/public/upgrade-preview) to
preview what the next financing tier requires — drives a tight feedback loop on the
metrics that move you forward.
See the [Lender Routing Recommendation](/api-reference/public/lender-routing) endpoint for
the live integration shape.
## Optional: brand attestation
Schema A brand attestations are issued operator-gated. They make your brand
cryptographically verifiable — an agent that sees "Your Brand" in a product feed can verify
the on-chain attestation rather than trust a string match.
To request one, email `support@droplinked.com` with your droplinked merchant ID and brand
materials. Once issued:
* Your brand slug binds to your wallet — impostor merchants can't claim it
* Verifiers (agents, downstream lenders, partners) can verify with a single read
* The Schema A attestation surfaces in agent discovery as a "verified brand" badge
See the [Trust Fabric Statistics](/api-reference/public/trust-fabric-stats) endpoint for
the public roll-up of how many brands are verified.
## What's coming
A realistic roadmap — what's live today is the integration above; these are the next
upgrades, in roughly the order we'll ship them:
* A direct **Shopify App Store** listing (replaces the webhook-config dance in Step 1)
* Auto-sync product images and variant matrices into InventoryOS (today we sync the
product record; rich media follows)
* Two-way inventory sync (opt-in, per-field) so you can run droplinked-side promotional
edits and have them flow back into Shopify
* **WooCommerce, BigCommerce, and Magento adapters** — same five-step pattern, different
webhook surfaces
* Customer-account cross-channel reconciliation so a buyer's Shopify-side order history
and droplinked-side affiliate history live behind one identity
## FAQ
No. Droplinked maintains a one-way read-only mirror — Shopify stays the source of truth
for your catalog, prices, descriptions, and stock levels.
No. Buyers still check out via your Shopify checkout, against your Shopify Payments
routing, with your tax and shipping rules. Droplinked only sees the order event after
Shopify writes it.
Yes. Revoke the webhooks from Shopify Admin → Settings → Notifications and the mirror
stops updating. Email `support@droplinked.com` to clear the droplinked-side merchant
entity if you want a clean slate.
Three sources: affiliate-network commissions on agent-sourced conversions (the 10%
agent share / 20% droplinked share of the 70/20/10 split), lender-side fees on
financing originated through the LenderRegistry, and premium MCP rate-limits / SLAs for
high-volume agent operators. Final pricing for the lender and premium-MCP tiers is
still being nailed — `support@droplinked.com` has the latest sheet.
Customer PII stays in Shopify. Droplinked only receives order events
(`orders/create`, `orders/updated`) and only the fields needed for attribution and
Schema C repayment-history attestation — not full customer profiles, addresses, or
payment data.
Yes. Operator-onboarded merchants get Schema A regardless of storefront source —
Shopify, WooCommerce, custom Next.js, or no storefront at all. The Shopify connector is
additive, not a prerequisite.
## Other platforms
The same five-step pattern works for the rest of the ecosystem — the webhook surface
changes per platform, the droplinked side is identical:
* **WooCommerce** — WooCommerce REST API webhooks at `wp-json/wc/v3/webhooks` (dedicated
guide coming at `/agentic/connect-woocommerce`)
* **BigCommerce** — V3 Webhooks API (`stores/{hash}/v3/hooks`) (dedicated guide coming at
`/agentic/connect-bigcommerce`)
* **Magento / Adobe Commerce** — Magento webhook module or a thin adapter against the REST
catalog API (dedicated guide coming)
* **Custom Next.js / React / headless storefronts** — register your own webhook emitter
against the droplinked ingestion endpoint; see [Connect your store](/agentic/connect-your-store)
for the platform-agnostic shape and [Storefront MCP
Discovery](/agentic/storefront-mcp-discovery) for the `` advertise
pattern.
## Related
* [Connect your store](/agentic/connect-your-store) — platform-agnostic connect surface
* [Storefront MCP Discovery](/agentic/storefront-mcp-discovery) — advertise your
per-merchant MCP URL from your storefront ``
* [For merchants](/concepts/for-merchants) — how droplinked composes with your existing
commerce stack
* [Trust fabric](/concepts/trust-fabric) — Schema A/B/C/D attestation architecture
* [Lender Routing Recommendation](/api-reference/public/lender-routing) — pick the right
lender by jurisdiction
* [Trust Fabric Statistics](/api-reference/public/trust-fabric-stats) — public rollup of
verified-brand counts and credit-attestation volumes
# Connect your store
Source: https://docs.droplinked.com/agentic/connect-your-store
Make a merchant's existing inventory agent-shoppable — meet customers where the agents already are.
Droplinked's thesis: **meet merchants where they already publish their inventory** and project
it into agentic surfaces. If you already sell on Droplinked — or connect an existing store —
your catalog becomes discoverable and purchasable by AI agents automatically. No re-platforming.
If you're connecting an existing **Shopify** store specifically (with step-by-step webhook setup, app-install instructions, catalog sync, and lender-routing pickup), see the dedicated [Connect your Shopify store](/agentic/connect-shopify) guide.
## What you get
Your products are published to the [Stripe ACP feed](/agentic/acp-feed) so agentic shopping
surfaces can ingest them.
Your catalog is queryable through the [MCP server](/agentic/mcp-server) by any agent.
Conversions settle through x402 + the 70/20/10 split, so the publishers and agents that
surface you are incentivized to.
## Per-merchant MCP surface
Beyond the platform-wide [MCP server](/agentic/mcp-server), every Droplinked shop is
projected as its own **path-prefixed MCP surface** at
`mcp.droplinked.com/{shopSlug}/...`. An agent that lands on a single storefront can pin to
just that shop's tools — no shop-slug filter argument needed on every call, no risk of
fanning out across the catalog.
### Endpoints (per merchant)
| Endpoint | Purpose |
| ----------------------------------------------------- | ---------------------------------------------------------------------------- |
| `GET mcp.droplinked.com/{shopSlug}/manifest` | Discoverable per-shop manifest (shop metadata + tool URLs) |
| `GET mcp.droplinked.com/{shopSlug}/mcp/tools` | Scoped tool list — same shape as the platform list, pre-bound to this shop |
| `POST mcp.droplinked.com/{shopSlug}/mcp/tools/{name}` | Invoke a scoped tool (e.g. `search_products`, `start_checkout`) on this shop |
### How agents discover it
Two paths, agent's choice:
* **Storefront-advertised** — your storefront page advertises its MCP URL via a meta tag the
agent reads at page-load time:
```html theme={null}
```
* **Canonical platform discovery** — the platform-wide well-known doc carries a
`merchantDiscovery.pathTemplate` an agent can substitute any shop slug into:
```bash theme={null}
# Agent discovers the platform-wide origin
curl -s https://mcp.droplinked.com/.well-known/mcp.json | jq .merchantDiscovery
# Pivot to a specific merchant
curl -s https://mcp.droplinked.com/your-shop-slug/manifest
```
Either way the result is the same: the agent ends up with a shop-scoped tool surface it can
call directly. See [MCP server](/agentic/mcp-server) for the platform-wide tool inventory
the per-merchant surface inherits.
For a complete guide to advertising your storefront's MCP URL via the `` tag (with snippets for custom Next.js, React, Shopify Liquid, and generic HTML storefronts), see [Storefront MCP Discovery](/agentic/storefront-mcp-discovery).
## Steps
List products on your Droplinked shop (physical, digital, or POD). The public catalog is
immediately available via `product-v2/public/shop/{shopName}` and flows into the ACP feed.
Confirm your items appear in the feed:
```bash theme={null}
curl "https://apiv3.droplinked.com/feed/acp.json" | jq '.items[] | select(.brand=="")'
```
Point an agent at the [MCP server](/agentic/mcp-server) and confirm `search_products` /
`list_shop_products` return your catalog.
Agent purchases route through Stripe ACP / x402. (`start_checkout` is finalizing as the
Stripe ACP enrollment completes — see the MCP status note.)
## Already hosting elsewhere?
Connecting an existing store surfaces the same catalog through Droplinked's commerce APIs and
the agentic layer — the goal is additive distribution, not migration. See the
[platform model](/concepts/platform-model) and [API reference](/api-reference/introduction)
for the import/connect surface.
# Consume Droplinked MCP
Source: https://docs.droplinked.com/agentic/consume-droplinked-mcp
Build an AI agent that searches, buys, and earns commission against the Droplinked catalog via the public MCP server.
This guide is for **agent developers** — anyone building on top of MCP-capable
clients (Claude, ChatGPT, Cursor, Continue, the Vercel AI SDK) who wants to
turn the Droplinked catalog into a tool surface their agent can call. For the
install side (point Claude Desktop / Claude Code at the local server), see
[MCP Server](/agentic/mcp-server). This page covers the **HTTP transport** at
`mcp.droplinked.com`.
The `mcp.droplinked.com` host, `X-MCP-API-Key` auth, and REST-style
`/mcp/tools/{name}` routes on this page describe the **hosted gateway**. The
Droplinked backend itself serves MCP directly at
`apiv3.droplinked.com` over JSON-RPC routes (`/mcp/v1/tools/{list,call}`),
unauthenticated in the current phase — see [MCP Server](/agentic/mcp-server)
and [Inventory MCP](/agentic/inventory-mcp) for the live, code-grounded
surface. If you're integrating against the backend today, use those.
## Endpoints
| Surface | URL | Auth |
| ----------------------- | ---------------------------------------------------- | --------------- |
| Discovery doc | `https://apiv3.droplinked.com/.well-known/mcp.json` | None |
| Platform-wide manifest | `https://mcp.droplinked.com/manifest` | None |
| Per-merchant manifest | `https://mcp.droplinked.com/{shopSlug}/manifest` | None |
| Platform-wide tool list | `https://mcp.droplinked.com/mcp/tools` | `X-MCP-API-Key` |
| Per-merchant tool list | `https://mcp.droplinked.com/{shopSlug}/mcp/tools` | `X-MCP-API-Key` |
| Tool invocation | `https://mcp.droplinked.com/mcp/tools/{name}` (POST) | `X-MCP-API-Key` |
Manifests and the well-known discovery doc are **unauthenticated** — any agent
can probe them at runtime to learn the surface.
## Authentication
For any tool invocation, send your key as the `X-MCP-API-Key` header:
```bash theme={null}
curl -X POST https://mcp.droplinked.com/mcp/tools/searchProducts \
-H "X-MCP-API-Key: $DROPLINKED_MCP_KEY" \
-H "content-type: application/json" \
-d '{"query": "wool beanie", "limit": 5}'
```
Request a key in the [Droplinked dashboard](https://droplinked.com) under
**Developer Settings → MCP Keys**. Keys are per-agent (one per integration is
recommended so you can revoke independently). Treat them like an API secret —
they identify your agent for attribution, rate-limiting, and the commission
rail.
The well-known discovery doc at
`https://apiv3.droplinked.com/.well-known/mcp.json` is the canonical source
of truth for the tool registry — names, descriptions, JSON-schema input
shapes, output shapes. Any compliant MCP client uses it for auto-discovery.
## Tool catalog
The full list (with input schemas + descriptions) is in the discovery doc.
The most relevant tools for an agentic shopping flow:
### Discovery
| Tool | Purpose |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `searchProducts` | Free-text search across the public catalog. Returns up to 25 matches with id, title, lowest SKU price, primary image, and product URL. Optional `shopUrl` filters to a single merchant. |
| `findInventory` | SKU-availability-aware multi-source query (`native` + live `impact_brand` + `any`). Returns stock state, source, price, an optional Model-A `trackedBuyUrl`, and attestation status. Designed for cart composition + multi-brand discovery. |
| `getProductDetail` | Full product detail — title, description, all images, all SKUs (price + inventory), tags, public URL. |
| `getShopInfo` | Public shop metadata (name, description, currency, logo, social links). Call before `getProductDetail` so the agent knows the shop's currency. |
| `listMerchants` | Paginated list of active merchants. Optional ISO-3166 region filter. |
### Transaction
| Tool | Purpose |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `placeOrderAgentic` | Compose an order from `[{skuId, quantity}]` + email. Phase 3 also returns the payment-intent (card / paypal / crypto / wallet slots) in the same call. PSP-invisible — the resolver picks the provider. Caps: 50 items, qty 1..99. |
| `getOrderReceipt` | PII-free order receipt — items, totals, payment state, fulfillment state. The `paymentMethod` field exposes the method category (`creditCard` / `paypal` / `crypto` / …), not the underlying PSP. |
| `getOrderStatus` | Lightweight state-machine projection for polling — status, payment state, fulfillment state, tracking, last-updated. No line items, no totals. |
| `refundOrder` | Issue a refund. Resolves the PSP automatically; supply `orderId` + optional partial amount + reason. Idempotency-keyed. |
### Trust + financing
| Tool | Purpose |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `recommendLender` | Recommend active lenders for a jurisdiction (ISO 3166-1 alpha-2). Required before initiating a lending application. |
| `getUnderwritingSignals` | Composite underwriting read for a merchant — credit tier, repayment history, upgrade preview. Privacy-aware. |
For the complete list (including admin tools, stockout monitoring, attestation
lineage, and cart operations), see
[Inventory MCP](/agentic/inventory-mcp),
[Underwriting MCP Tools](/agentic/underwriting-mcp-tools), and
[Lender Trinity MCP Tools](/agentic/lender-trinity-mcp-tools).
## End-to-end example
A canonical agentic shopping loop: search → detail → order → receipt.
```bash curl theme={null}
# 1. Search
curl -s -X POST https://mcp.droplinked.com/mcp/tools/searchProducts \
-H "X-MCP-API-Key: $DROPLINKED_MCP_KEY" \
-H "content-type: application/json" \
-d '{"query": "wool beanie", "limit": 5}'
# 2. Detail (pick a result)
curl -s -X POST https://mcp.droplinked.com/mcp/tools/getProductDetail \
-H "X-MCP-API-Key: $DROPLINKED_MCP_KEY" \
-H "content-type: application/json" \
-d '{"shopUrl": "warmwool", "productSlug": "classic-wool-beanie"}'
# 3. Place order
curl -s -X POST https://mcp.droplinked.com/mcp/tools/placeOrderAgentic \
-H "X-MCP-API-Key: $DROPLINKED_MCP_KEY" \
-H "content-type: application/json" \
-d '{
"shopUrl": "warmwool",
"email": "shopper@example.com",
"items": [{"skuId": "65f…", "quantity": 1}],
"returnPaymentIntent": true
}'
# 4. Poll status / pull receipt
curl -s -X POST https://mcp.droplinked.com/mcp/tools/getOrderReceipt \
-H "X-MCP-API-Key: $DROPLINKED_MCP_KEY" \
-H "content-type: application/json" \
-d '{"orderId": "ord_…"}'
```
```typescript typescript theme={null}
const MCP = 'https://mcp.droplinked.com';
const headers = {
'X-MCP-API-Key': process.env.DROPLINKED_MCP_KEY!,
'content-type': 'application/json',
};
async function call(tool: string, args: Record): Promise {
const res = await fetch(`${MCP}/mcp/tools/${tool}`, {
method: 'POST',
headers,
body: JSON.stringify(args),
});
if (!res.ok) throw new Error(`${tool}: ${res.status} ${await res.text()}`);
return res.json();
}
// 1. Search
const results = await call<{items: Array<{id: string; title: string; lowestPriceCents: number}>}>(
'searchProducts',
{query: 'wool beanie', limit: 5},
);
// 2. Detail
const detail = await call<{skus: Array<{id: string; priceCents: number; inventory: number}>}>(
'getProductDetail',
{shopUrl: 'warmwool', productSlug: 'classic-wool-beanie'},
);
// 3. Order
const order = await call<{orderId: string; paymentIntent: {redirectUrl: string}}>(
'placeOrderAgentic',
{
shopUrl: 'warmwool',
email: 'shopper@example.com',
items: [{skuId: detail.skus[0].id, quantity: 1}],
returnPaymentIntent: true,
},
);
// 4. Receipt
const receipt = await call('getOrderReceipt', {orderId: order.orderId});
```
```python python theme={null}
import os, requests
MCP = 'https://mcp.droplinked.com'
HEADERS = {
'X-MCP-API-Key': os.environ['DROPLINKED_MCP_KEY'],
'content-type': 'application/json',
}
def call(tool, args):
r = requests.post(f'{MCP}/mcp/tools/{tool}', headers=HEADERS, json=args, timeout=10)
r.raise_for_status()
return r.json()
# 1. Search
results = call('searchProducts', {'query': 'wool beanie', 'limit': 5})
# 2. Detail
detail = call('getProductDetail', {'shopUrl': 'warmwool', 'productSlug': 'classic-wool-beanie'})
# 3. Order
order = call('placeOrderAgentic', {
'shopUrl': 'warmwool',
'email': 'shopper@example.com',
'items': [{'skuId': detail['skus'][0]['id'], 'quantity': 1}],
'returnPaymentIntent': True,
})
# 4. Receipt
receipt = call('getOrderReceipt', {'orderId': order['orderId']})
```
## Per-merchant pinning
When the user arrives at a single storefront, scope your tool calls to that
shop's per-merchant surface — no `shopUrl` filter argument, no risk of
fanning out across the catalog:
```bash theme={null}
# Discovery
curl -s https://mcp.droplinked.com/warmwool/manifest
# Pre-bound tool list
curl -s https://mcp.droplinked.com/warmwool/mcp/tools \
-H "X-MCP-API-Key: $DROPLINKED_MCP_KEY"
# Invoke a tool, pre-scoped to warmwool's catalog
curl -s -X POST https://mcp.droplinked.com/warmwool/mcp/tools/searchProducts \
-H "X-MCP-API-Key: $DROPLINKED_MCP_KEY" \
-H "content-type: application/json" \
-d '{"query": "beanie"}'
```
The per-merchant surface inherits the platform-wide tool catalog but
pre-scopes every call. See [Storefront MCP Discovery](/agentic/storefront-mcp-discovery)
for the `` advertisement pattern.
## Attribution
Every tool call you make against `mcp.droplinked.com` is tagged with the
API key, which Droplinked maps to your agent identity. When a downstream
conversion (`placeOrderAgentic` → payment-captured) is attributed to your
agent, the **10% affiliate share** in the
[70/20/10 split](/agentic/attribution-and-commission) routes to you.
Through the hosted gateway, attribution is implicit in the API-key-signed
tool-call chain. Against the backend directly (no signing key), call
`recordAgenticIntent` to register intent against an opted-in merchant — that
ledger row is what the settlement coordinator attributes the conversion to. See
[Attribution + Commission](/agentic/attribution-and-commission).
## Rate limits + observability
* Default: 600 tool calls / minute / API key. Burst headroom up to 1000/min.
* 429 responses include `Retry-After` headers and a body listing the
exceeded budget.
* Discovery + manifest endpoints are unrate-limited.
* All requests are logged with `correlationId` headers — quote them in
support tickets for fast diagnosis.
## Next steps
Understand the catalog metadata your agent will rank against — so you can
surface the best candidates.
Full walkthrough of how an agent-driven conversion routes USDC + fiat back
to you.
# Get listed in MCP discovery
Source: https://docs.droplinked.com/agentic/get-listed-in-mcp-discovery
Opt your existing catalog into Droplinked's agentic discovery surface — meet AI shoppers where they already are, with a new performance-based commission rail.
Droplinked projects your existing catalog into the agentic surfaces AI shoppers
already use — ChatGPT, Claude, Cursor, and any MCP-capable client. Listing your
shop publishes it to the [ACP feed](/agentic/acp-feed), the platform-wide
[MCP server](/agentic/mcp-server), and a per-merchant MCP surface at
`mcp.droplinked.com/{shopSlug}`. No re-platforming, no new checkout. Additive
distribution on top of the commerce you already run.
## What MCP discovery actually is
**MCP** is the Model Context Protocol — the open standard agents like Claude,
ChatGPT, Cursor, and Continue use to call tools at runtime. Droplinked runs an
MCP server that exposes your catalog as a tool surface
(`searchProducts`, `findInventory`, `getProductDetail`, `placeOrderAgentic`,
`getOrderReceipt`). When an AI agent answers "find me a black wool beanie under
\$40," the agent calls those tools, your inventory appears in the results, and a
purchase routes through the existing storefront chokepoint.
## Why opt in
Your catalog becomes a result candidate in agentic shopping sessions across
every MCP-aware client.
Conversions attributed to an agent settle through the 70/20/10 split —
you keep 70%, the agent that surfaced you earns 10%, Droplinked takes 20%
for the discovery + settlement layer.
Your storefront, PSPs, and fulfillment stack stay exactly as they are.
Discovery is purely additive.
## How to opt in
Any Droplinked shop with at least one published product flows into the
ACP feed and the public MCP catalog automatically. If you connect a Shopify
or other storefront, finish that onboarding first — see
[Connect your store](/agentic/connect-your-store) or the
[Connect Shopify](/agentic/connect-shopify) walkthrough.
```bash theme={null}
curl -s "https://apiv3.droplinked.com/feed/acp.json" \
| jq '.items[] | select(.brand == "Your Brand Name") | {id, title, link, price}' \
| head -20
```
If items return, you are live in agentic discovery. If not, see the
pre-flight checklist below.
```bash theme={null}
curl -s "https://mcp.droplinked.com/your-shop-slug/manifest" | jq .
```
A 200 with shop metadata + tool URLs confirms agents can pin to your shop
directly. A 404 means the slug isn't registered — confirm the slug in your
dashboard.
Add one line to your storefront `` so agents that arrive at your shop
URL can pivot straight to your MCP surface:
```html theme={null}
```
Full instructions: [Storefront MCP Discovery](/agentic/storefront-mcp-discovery).
## Catalog pre-flight checklist
Agents converge on a small set of fields when ranking candidates. Make sure
each product has:
* **Title** — descriptive, agent-readable (see
[AEO/GEO optimization](/agentic/aeo-geo-optimization))
* **Description** — at least one full sentence
* **Primary image** — public-accessible URL, square or 1:1 preferred
* **Price + currency** — must resolve to a non-zero amount in a supported
currency
* **Availability** — at least one SKU with stock state `in_stock` or
`low_stock`
* **Public visibility** — product not gated behind a draft / unpublished
flag
* **Brand** — fills the `brand` field on the ACP feed item
* **`item_group_id`** — variants of the same product grouped under a parent
* **Condition** — `new` / `refurbished` / `used`
* **Structured-data optimized titles + descriptions** — see
[AEO/GEO optimization](/agentic/aeo-geo-optimization)
## Pricing + commission
Listing your catalog in MCP discovery is **free**. You pay only on
agent-attributed conversions, on a 70/20/10 split:
| Party | Share | Description |
| ------------------ | ------- | ------------------------------------------------------ |
| You (merchant) | **70%** | Net of fulfillment, PSP fees, and the 30% routed below |
| AI-agent affiliate | **10%** | The agent that surfaced your product to the buyer |
| Droplinked | **20%** | Discovery + settlement + the agentic infrastructure |
Settlement runs on two rails:
* **USDC on Base via x402** — micropayment streaming, suitable for high-volume
agent traffic. Settles continuously.
* **Fiat off-ramp via Stripe** — weekly payout to your Stripe-connected bank
account.
You can mix the two. See
[Attribution + commission](/agentic/attribution-and-commission) for the
detailed flow.
## Privacy + control
The discovery surface exposes only **public catalog metadata** — shop name,
slug, product titles, descriptions, prices, images, availability. It does
**not** expose PII, operator data, sales volume, or PSP routing decisions.
Per-merchant attestation policies (`public`, `lenders_only`, `private`)
govern any deeper inventory + attestation surface — see
[Inventory MCP](/agentic/inventory-mcp).
You can opt out at any time by un-publishing the shop or contacting support.
## Troubleshooting
Most common causes, in order:
1. No published products — drafts don't appear
2. All SKUs marked out of stock
3. The shop slug isn't matching the `brand` field — try filtering by `.link`
containing your storefront URL instead
4. The feed regenerates on product write plus a short refresh cycle — wait
5 minutes after publishing
* Verify the slug exactly matches the one in your dashboard URL
* The slug is case-sensitive and dash-separated
* If you recently renamed your shop, the slug may have changed — old slugs
don't redirect
## Next steps
Apply answer-engine + generative-engine optimization to your titles and
descriptions so agents rank you higher.
Detailed walkthrough of how an agent-driven conversion is attributed and
settled.
# Inventory MCP
Source: https://docs.droplinked.com/agentic/inventory-mcp
Droplinked's Model Context Protocol surface — the live tool registry any AI agent can discover and call against the apiv3 backend.
The **Inventory MCP** is Droplinked's agentic surface over [InventoryOS](/concepts/inventory-os).
Any MCP-capable agent (Claude, ChatGPT, Cursor, Continue, the Vercel AI SDK, …) can discover
merchant catalogs, compose carts, place orders, issue refunds, and read trust-fabric state —
served directly by the Droplinked backend (`apiv3`), no extra gateway and (in the current phase)
no credentials required for the public tools.
Discovery doc (live):
```bash theme={null}
curl -s https://apiv3.droplinked.com/.well-known/mcp.json | jq '.tools[].name'
```
This page is grounded in the live tool registry
(`McpToolRegistryService`). The discovery doc at
`/.well-known/mcp.json` is always the canonical source of truth for the exact
tool names, descriptions, and JSON-schema input shapes — if this page and the
discovery doc disagree, the discovery doc wins.
## Transport + protocol
The backend speaks MCP over HTTP. See [MCP Server](/agentic/mcp-server) for the full transport
reference. In short:
| Surface | Method + path | Auth |
| ---------------------- | ------------------------------------------ | ----------------------------------------------------- |
| Global discovery doc | `GET /.well-known/mcp.json` | Public |
| Per-shop discovery doc | `GET /shop/{shopUrl}/.well-known/mcp.json` | Public |
| List tools | `POST /mcp/v1/tools/list` | Public |
| Call a tool | `POST /mcp/v1/tools/call` | Public (admin tools gated by an `_adminKey` argument) |
| List resources | `POST /mcp/v1/resources/list` | Public |
| Read a resource | `POST /mcp/v1/resources/read` | Public |
All `/mcp/v1/*` routes accept **both** a flat body (`{ name, arguments }`) and a
**JSON-RPC 2.0** envelope (`{ jsonrpc: "2.0", id, method, params }`) — the shape every official
MCP client sends. The response mirrors the request shape. The advertised `protocolVersion` is
`2024-11-05`.
## The tool registry
The registry currently exposes **25 tools**. Tools are read-only unless marked **write**.
Three observability/diagnostic tools are **admin-gated**: they require an `_adminKey` argument
that matches the server's `MCP_ADMIN_SECRET`.
### Catalog + discovery (read-only)
| Tool | Purpose |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `searchProducts` | Free-text search across the public Droplinked catalog (matches product title). |
| `findInventory` | SKU-availability-aware multi-source query (`native` + live `impact_brand` + `any`) — returns `stock_state`, `source`, `priceCents`, `currency`, `imageUrl`, `productUrl`, an optional Model-A `trackedBuyUrl`, and a (policy-redacted) attestation status. Designed for cart composition + multi-brand discovery. See [below](#about-findinventory). |
| `getProductDetail` | Full product detail for a `shopUrl` + `productSlug` — images, all SKUs (price + inventory), metadata. |
| `getShopInfo` | Public shop metadata — name, description, currency, logo, social links. |
| `listMerchants` | List active merchants (as their public shops), optional ISO-3166 country filter. |
| `getStockoutAlerts` | SKUs at stockout risk for a shop, severity-filtered. Attestation-policy-aware. |
| `getInventoryHealth` | One-shop inventory posture snapshot — total SKUs, stockouts, low-stock, attestation status, synced connectors. |
| `estimateIntakeCost` | Estimate the monthly infra cost of adding N SKUs to a merchant's Droplinked catalog. |
### Merchant discovery — opt-in only (read-only, except where noted)
These tools only surface merchants who have set `agenticDiscoveryEnabled: true`. See
[Merchant Discovery Rules](/agentic/merchant-discovery-rules).
| Tool | Purpose |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `searchMerchants` | Search opted-in merchants by query / category / geo / currency. |
| `getMerchantDirectory` | Paginated directory of opted-in merchants. |
| `getMerchantCatalogSummary` | Catalog snapshot for an opted-in merchant — product count, top categories, currency, shipping geos. |
| `recordAgenticIntent` | **Write.** Log an external agent's intent (`browse` / `buy` / `compare` / `share`) against an opted-in merchant. Feeds the attribution ledger. Idempotent on an optional `intentId`. |
### Cart + order (write, except reads)
| Tool | Purpose |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `cartAdd` | **Write.** Add a line to a cart (or create a cart for a shop). |
| `cartUpdateLine` | **Write.** Change quantity on an existing cart line. |
| `cartRemoveLine` | **Write.** Remove a line from a cart. |
| `cartApplyDiscount` | **Write.** Apply a discount / coupon code to a cart. |
| `placeOrderAgentic` | **Write.** Compose an order from `[{skuId, quantity}]` + email; optionally resolve the payment-intent in the same call (PSP-invisible). |
| `getOrderReceipt` | PII-free, PSP-invisible order receipt — line items, totals, payment state, fulfillment. |
| `getOrderStatus` | Lightweight order state-machine projection for polling — status, payment state, fulfillment, tracking. |
| `refundOrder` | **Write.** Issue a refund; the PSP is resolved automatically from the order record. |
### Trust + financing (read-only)
| Tool | Purpose | Notes |
| ------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `recommendLender` | Recommend active lenders for a jurisdiction (exact match first, then `GLOBAL`). | See [Lender Trinity MCP Tools](/agentic/lender-trinity-mcp-tools). |
| `getUnderwritingSignals` | Composite underwriting read — Schema B credit-risk + Schema C repayment history + tier upgrade preview. | Privacy-gated to opted-in merchants. See [Underwriting MCP Tools](/agentic/underwriting-mcp-tools). |
| `traceAttestationLineage` | Walk the cross-schema EAS attestation lineage tree for a merchant. | **Admin-gated** (`_adminKey`). |
### Observability / diagnostics (admin-gated, read-only)
| Tool | Purpose |
| ------------------- | ---------------------------------------------------------------------------------------------------------- |
| `queryEventLog` | Query the EventBus event log (merchant / type / time-range, keyset-paginated). Requires `_adminKey`. |
| `getInventoryDrift` | Compare Droplinked-mirrored inventory against the Shopify Admin API source of truth. Requires `_adminKey`. |
## Tool annotations (preview)
The MCP spec lets a server tag each tool with **behavioural hints** so a client can reason about
safety before calling — `readOnlyHint` (does not mutate state), `destructiveHint` (may perform
an irreversible update), and `openWorldHint` (touches an external system). These annotations ship
alongside the [Streamable HTTP transport](/agentic/mcp-server#planned-streamable-http) (gated
behind `MCP_STREAMABLE_HTTP_ENABLED`, preview) — the current `/mcp/v1/*` `tools/list` response
does not yet include them, so treat this table as the planned classification:
| Class | Tools | `readOnlyHint` | `destructiveHint` |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------------- |
| Read-only discovery / fetch | `findInventory`, `searchProducts`, `searchMerchants`, `getProductDetail`, `getShopInfo`, `getOrderReceipt`, `getOrderStatus`, `getStockoutAlerts`, `getInventoryHealth`, `getInventoryDrift`, `getUnderwritingSignals`, `getMerchantDirectory`, `getMerchantCatalogSummary`, `listMerchants`, `traceAttestationLineage`, `recommendLender`, `estimateIntakeCost`, `queryEventLog` | `true` | — |
| Recoverable writes | `cartAdd`, `cartUpdateLine`, `cartRemoveLine`, `cartApplyDiscount`, `recordAgenticIntent` | `false` | — |
| Destructive writes (money moves) | `placeOrderAgentic`, `refundOrder` | `false` | `true` |
`findInventory` and `getInventoryDrift` additionally carry `openWorldHint: true` (they reach
Impact / the Shopify Admin API).
## About `findInventory`
`findInventory` is the cart-composition + multi-brand discovery primitive — it queries across
inventory sources and returns each match with stock state, price, image, and a buyable product
URL, so an agent can build a cart across results.
The `source` argument selects the inventory source: `native`, `impact_brand`, `rakuten`, or
`any` (union, deduplicated by SKU):
* **`native`** — the Droplinked catalog (published, visible, purchasable `ProductV2` /
`ProductSkuV2`). Always live.
* **`impact_brand`** — the **live multi-brand discovery path**. When
`IMPACT_LIVE_SEARCH_ENABLED=true`, `findInventory` proxies Impact's live
`Catalogs/ItemSearch` endpoint over the full multi-brand advertiser catalog
(keyword-matched as `Name~""`), with a thin \~6h per-keyword cache and **zero
bulk replication**. This is how an agent reaches brands like Walmart or Lids through the
same `findInventory` call. The path is **fail-open**: on a rate-limit (Impact's quota is
hourly) or any upstream error it serves a stale cache hit if present, otherwise degrades to
native results — it never breaks the call. With the flag **off**, `impact_brand` returns no
live rows and the call falls back to native results.
* **`rakuten`** — reserved for the Rakuten feed; returns no rows until that source is wired.
When an `intentId` (from `recordAgenticIntent`) is supplied and `IMPACT_ATTRIBUTION_ENABLED` is
on, each `impact_brand` result is enriched with a **Model-A `trackedBuyUrl`** — a tracked
deep-link the agent should drive the buyer to, so the advertiser's pixel credits Droplinked
(publisher), tagged `subId1=dl-agentic:{intentId}`. Enrichment is per-item fail-open: a
link-gen failure returns the product *without* a `trackedBuyUrl` rather than dropping it. See
[Attribution + Commission](/agentic/attribution-and-commission#external-impact-advertiser-path).
The per-item `attestation` field is a privacy-redacted status (`unattested` by default for
external `impact_brand` items, which carry no native attestation) and respects each merchant's
attestation policy for native items, so it never over-discloses.
When `DISCOVERY_RANKER_ENABLED=true`, results are re-ordered by their merchant's
attestation-weighted trust score (and each item carries a `trustScore`). With the flag off,
results come back in source order.
```bash theme={null}
curl -s -X POST https://apiv3.droplinked.com/mcp/v1/tools/call \
-H 'content-type: application/json' \
-d '{
"name": "findInventory",
"arguments": { "query": "beanie", "inStockOnly": true, "limit": 5 }
}' | jq '.content[0].text | fromjson'
```
## Multi-brand Impact catalog
The large multi-brand affiliate catalog (Flatlay × Impact Partner REST) is reachable two ways:
1. **Through `findInventory`** (`source: impact_brand` / `any`) — the public MCP path. When
`IMPACT_LIVE_SEARCH_ENABLED=true` this proxies Impact's live `Catalogs/ItemSearch` over the
whole multi-brand advertiser catalog (keyword `Name~""`), query-time, never
bulk-replicated, with a thin \~6h per-keyword cache. This is the primary agent-facing path
and is described in [About `findInventory`](#about-findinventory) above.
2. **Through a merchant-authenticated connector** — a separate, **not** public surface for a
merchant to manage their own Impact connection. It is read-only, query-time (never
bulk-replicated), and rate-limited per merchant.
| Endpoint | Purpose |
| --------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `POST /connectors/impact/connect` | Connect a merchant's Impact `accountSid` + `authToken` (validated before persisting, encrypted at rest). |
| `GET /connectors/impact/status` | Connection state + imported brand count. |
| `GET /connectors/impact/brands` | Imported brand metadata for the merchant. |
| `GET /connectors/impact/items` | Query catalog items in real time (filter by `brandId`, `search`, paginated). |
All four connector endpoints require a merchant JWT (PRODUCER role). The connector **never**
exposes write methods, even when the token carries RW scope.
## Privacy + cost-efficiency
Inventory + attestation tools respect each merchant's `MerchantAttestationPolicy`:
* Default: **private** — merchants who have not opted in see a redacted attestation status.
* **`lenders_only` scope** — visible to allowlisted lenders.
* **`public` scope** — visible to anyone.
Merchant-discovery tools (`searchMerchants`, `getMerchantDirectory`,
`getMerchantCatalogSummary`, `recordAgenticIntent`) hard-filter on `agenticDiscoveryEnabled` —
non-opted merchants are invisible, and `recordAgenticIntent` returns `opt_in_required` rather
than writing a ledger row.
## Where to go next
* [MCP Server](/agentic/mcp-server) — full transport reference + how to connect Claude / ChatGPT / Cursor
* [Attribution + Commission](/agentic/attribution-and-commission) — how agentic conversions settle
* [Merchant Discovery Rules](/agentic/merchant-discovery-rules) — opt-in + tiering
* [Storefront MCP Discovery](/agentic/storefront-mcp-discovery) — per-shop discovery doc
* [InventoryOS concept](/concepts/inventory-os) — the architectural model underneath
# Lender Product Catalog
Source: https://docs.droplinked.com/agentic/lender-product-catalog
The 6-product financial-instrument catalog lenders opt into, how routing matches merchants against it, and how Lender.excludedSectors[] applies a hard sector filter.
Droplinked's lender layer doesn't assume every lender does the same thing. Some do
revenue-based finance, some do receivables, some do payables, some do invoice discounting,
some run a Sharia-compliant interest-free variant. The **Lender Product Catalog** is the
in-code list of the 6 financial-instrument types Droplinked currently supports; each lender
opts in via `LenderProductOffering` rows that attach a product type to a
`(lenderId, productType, jurisdiction)` triple, with their own yield band + ticket envelope.
Routing then matches a merchant's funding request against the active offerings — pre-filtered
by the lender's `excludedSectors[]` so a jewelry shop never sees a lender that has opted
out of jewelry, even if the envelope would otherwise match.
***
## At a glance
| | |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Base URL** | `https://apiv3.droplinked.com` |
| **Catalog version** | `2026.06.15` (in-code, exposed via `versionAtWrite` on routing responses) |
| **Product types** | 6 (RBF, INTEREST\_FREE\_RBF, RECEIVABLES, PAYABLES, LINE\_OF\_CREDIT, INVOICE\_DISCOUNTING) |
| **Storage** | `LenderProductOffering` Mongo schema, one row per `(lenderId, productType, jurisdiction)` |
| **Sector filter** | `Lender.excludedSectors[]` free-string array (catalog suggests 9 common values) |
| **Public endpoints** | 4 (`/v2/lender-product-catalog`, `/v2/lender-product-catalog/matches`, `/v2/lender-product-catalog/lender/:lenderId`, `/v2/lender-routing/recommend`) |
| **Admin endpoints** | 4 (`POST/PATCH/DELETE /admin/lender-product-offerings`, `PATCH /admin/lenders/:lenderId/excluded-sectors`) |
***
## The 6 product types
| Type | Display name | Typical lender yield (bps) | Typical ticket (USD) | Typical tenor (months) | Typical industries |
| ----------------------- | ----------------------------------- | -------------------------- | -------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `REVENUE_BASED_FINANCE` | Revenue-Based Finance | 1,500 – 2,500 | 500K – 2.7M | 6 – 12 | retail, e-commerce, marketplace, pos, payment-gateways |
| `INTEREST_FREE_RBF` | Interest-Free Revenue-Based Finance | 900 – 1,200 | 68K – 270K | 3 – 6 | retail, e-commerce, marketplace, pos, payment-gateways |
| `RECEIVABLES_FINANCE` | Receivables Finance | 1,800 – 2,500 | 545K – 2.7M | 12 | staffing, sales-agencies, security-services, advertising, logistics, it-services, professional-services, industrial-services, healthcare-services, training-providers |
| `PAYABLES_FINANCE` | Payables Finance | 1,800 – 2,500 | 950K – 2.7M | 12 | manufacturing, trading, distribution |
| `LINE_OF_CREDIT` | Line of Credit | 1,800 – 3,000 | 545K – 2.7M | 6 – 24 | retail, e-commerce, staffing, services, trading |
| `INVOICE_DISCOUNTING` | Invoice Discounting | 1,800 – 2,500 | 100K – 2.7M | 1 – 4 | services, b2b, professional-services, logistics |
These are the *catalog typicals* — defaults sourced from public UAE / GCC SME-credit market
data. They are **not** what a given lender will offer; each lender's actual band is set on
their `LenderProductOffering` rows and overrides the catalog typical at routing time.
Yield is denominated in **basis points** (100 bps = 1%). Ticket envelopes are denominated in
**USD** in the catalog, then translated per jurisdiction at the offering level (e.g. AED for
CredibleX-UAE rows).
***
## For lenders: how to opt in
A lender doesn't appear in any routing response until they have at least one
`LenderProductOffering` row marked `status: 'ACTIVE'`. The opt-in flow:
1. Lender (or their onboarding rep) browses `GET /v2/lender-product-catalog` to see the 6
product types and their typical bands.
2. Lender picks the products they want to support and, per product, the jurisdictions they
want to lend into (often a single jurisdiction per lender; can be multi).
3. Operator runs `POST /admin/lender-product-offerings` once per
`(lenderId, productType, jurisdiction)` triple with the lender's actual yield band +
envelope. The lender can override every catalog typical with their real numbers — and
the routing layer uses those numbers, not the catalog defaults.
Example admin body for one offering:
```json theme={null}
POST /admin/lender-product-offerings
{
"lenderId": "crediblex-uae",
"productType": "REVENUE_BASED_FINANCE",
"jurisdiction": "AE",
"status": "ACTIVE",
"lenderYieldBpsMin": 1700,
"lenderYieldBpsMax": 2200,
"ticketMin": 2000000,
"ticketMax": 10000000,
"ticketCurrency": "AED",
"tenorMonthsMin": 6,
"tenorMonthsMax": 12,
"minMonthlyRevenue": 20000000,
"minMonthlyRevenueCurrency": "AED",
"supportedIndustries": ["retail", "e-commerce", "marketplace"]
}
```
When `lenderYieldBpsMin`/`Max` are supplied they **override** the catalog typical. When omitted
they fall through to the catalog default. Same applies to the ticket envelope and tenor.
`status` flips between `ACTIVE` (visible to routing) and `PAUSED` (hidden, kept for audit).
Deleting an offering is rare — operators prefer `PAUSED` so the lender can re-enable without
re-onboarding.
***
## For merchants: how routing matches
A merchant doesn't talk to the catalog directly. They request credit through the FE, which
calls the lender-routing recommendation endpoint:
```
GET /v2/lender-routing/recommend
?jurisdiction=AE
&productType=REVENUE_BASED_FINANCE
&monthlyRevenueAed=1500000
&requestedTicketAed=3000000
&merchantIndustry=e-commerce
&merchantSector=apparel
```
The routing service walks four filters in order:
1. **Jurisdiction** — only lenders that have offerings in the merchant's jurisdiction.
2. **Product** — only offerings whose `productType` matches the request (when supplied —
the `productType` filter is **opt-in**, legacy callers without it are unchanged).
3. **Envelope** — only offerings where `monthlyRevenueAed >= minMonthlyRevenue` and
`requestedTicketAed` falls inside `[ticketMin, ticketMax]` and the merchant's
`merchantIndustry` is in `supportedIndustries` (or the offering's industry list is empty).
4. **Sector** — any lender whose `Lender.excludedSectors[]` contains the merchant's
`merchantSector` is dropped, regardless of envelope match.
The response carries a `matchingOfferings[]` list so the FE can render per-lender competitive
quotes:
```json theme={null}
{
"jurisdiction": "AE",
"productType": "REVENUE_BASED_FINANCE",
"versionAtWrite": "2026.06.15",
"count": 1,
"matchingOfferings": [
{
"lenderId": "crediblex-uae",
"displayName": "CredibleX (UAE)",
"productType": "REVENUE_BASED_FINANCE",
"lenderYieldBpsMin": 1700,
"lenderYieldBpsMax": 2200,
"ticketMin": 2000000,
"ticketMax": 10000000,
"ticketCurrency": "AED",
"tenorMonthsMin": 6,
"tenorMonthsMax": 12
}
]
}
```
The FE can then render "CredibleX competes on RBF at 17 – 22% yield, ticket AED 2Mn – 10Mn,
tenor 6 – 12 months." If multiple lenders match, each one is its own row and the merchant
picks.
***
## Excluded sectors
`Lender.excludedSectors[]` is a free-string array on the lender document — a hard filter
applied **after** envelope match. The catalog ships a list of 9 common sector strings as a
UX hint so operators can pick from a consistent vocabulary, but lenders are free to add
custom strings outside the suggested list.
| Sector string | Note |
| --------------------------------------- | ------------------------------------- |
| `jewelry-and-bullion-trading` | Common high-value-goods AML carve-out |
| `currency-exchange-and-money-transfer` | FATF-sensitive |
| `unlicensed-financial-institutions` | Regulatory hard no |
| `auction-houses-and-antique-dealers` | High-value-goods AML carve-out |
| `trusts-or-fund-management` | Regulatory carve-out |
| `gambling-casino-or-betting-related` | Common ESG / Sharia carve-out |
| `charities-and-not-for-profit` | FATF terror-finance sensitive |
| `arms-and-ammunition-trading` | Hard ESG carve-out |
| `real-estate-development-and-brokerage` | Common large-ticket carve-out in MENA |
The catalog list is *suggestion only*. Routing only checks string equality against
`Lender.excludedSectors[]`; the source of truth is the lender's own array.
Operators edit a lender's exclusions via `PATCH /admin/lenders/:lenderId/excluded-sectors`
with the full replacement array — not a delta.
***
## CredibleX-UAE reference example
CredibleX is the launch FSRA-licensed lender in jurisdiction AE. They are seeded on
production with 4 ACTIVE offerings and 9 excluded sectors (all of the common 9 above),
sourced from the [UAE Qualifying Business Terms 2026](https://www.crediblex.com/) public
underwriting policy.
| Product | Min monthly revenue | Ticket range | Tenor |
| ----------------------- | -------------------------------------------- | -------------- | ------------- |
| `REVENUE_BASED_FINANCE` | AED 20M | AED 2M – 10M | 6 – 12 months |
| `INTEREST_FREE_RBF` | AED 2M | AED 250K – 10M | 3 months |
| `RECEIVABLES_FINANCE` | AED 15M | AED 2M – 10M | 12 months |
| `PAYABLES_FINANCE` | AED 100M (trading) / AED 30M (manufacturing) | AED 3.5M – 10M | 12 months |
A merchant in AE doing AED 1.5M / month in revenue, asking for AED 3M of RBF, will **not**
match CredibleX's RBF offering (revenue floor is AED 20M). They will match the
`INTEREST_FREE_RBF` offering (revenue floor AED 2M) — and the routing response will show
that competitive line.
***
## Related
* [Inventory MCP](/agentic/inventory-mcp) — catalog discovery for agents; mostly orthogonal
to lender routing but worth knowing
* [Lender Trinity MCP Tools](/agentic/lender-trinity-mcp-tools) — `verify_lender` /
`recommend_lender` / `recommend_service_provider`; this catalog feeds the recommender
* [Trust fabric](/concepts/trust-fabric) — Schema A / B / C / D EAS attestation chain
context; the `LenderRegistry` is the entity layer this catalog sits on top of
* [Merchant Discovery Rules](/agentic/merchant-discovery-rules) — KYB visibility rules for
the discovery surface; same `verifiedTier` gate applies before a merchant can request
credit through the routing layer
# Lender Trinity MCP Tools
Source: https://docs.droplinked.com/agentic/lender-trinity-mcp-tools
3 new MCP tools wrapping the LenderRegistry trinity public surface: verify_lender, recommend_lender, recommend_service_provider.
Three agent-callable MCP tools wrap the [LenderRegistry trinity](/concepts/trust-fabric) public surface, letting consumer agents resolve credit-risk attestations to their issuer + recommend lenders/service providers for a given merchant — without operator handholding.
## `verify_lender`
Resolves a `lenderId` (referenced in a Schema B attestation) to the lender's public profile + signing wallet.
```typescript theme={null}
// Agent call
const profile = await mcp.callTool('verify_lender', { lenderId: 'crediblex-uae' });
```
```json theme={null}
{
"found": true,
"lenderId": "crediblex-uae",
"displayName": "CredibleX (UAE)",
"archetype": "fsra-licensed",
"jurisdiction": "AE",
"status": "ACTIVE",
"signingWallet": "0x...",
"regulatorReference": "FSRA-12345",
"issuedAttestationCount": 42,
"lastAttestationAt": "2026-06-11T22:00:00Z"
}
```
**Use case**: A consumer agent has parsed a Schema B attestation and needs to display the issuing lender's human-readable name + regulator reference. Forensic cross-check between `signingWallet` and the on-chain `issuerWallet` detects schema impersonation.
Wraps: [`GET /v2/lenders/:lenderId`](/api-reference/public/lender-registry).
## `get_lender_history`
Trace a lender's full lifecycle — REGISTERED, STATUS\_CHANGED, metadata edits. Verifiers use this to determine whether a lender was ACTIVE at the time a Schema B credit-risk attestation was minted, and to surface any SUSPENDED / ARCHIVED transitions.
```typescript theme={null}
const history = await mcp.callTool('get_lender_history', { lenderId: 'crediblex-uae' });
```
```json theme={null}
{
"lenderId": "crediblex-uae",
"count": 3,
"events": [
{
"occurredAt": "2026-05-01T09:15:00Z",
"eventType": "LENDER_REGISTERED",
"previousStatus": null,
"newStatus": "PENDING_KYB"
},
{
"occurredAt": "2026-05-03T14:22:00Z",
"eventType": "LENDER_STATUS_CHANGED",
"previousStatus": "PENDING_KYB",
"newStatus": "ACTIVE"
},
{
"occurredAt": "2026-05-20T11:00:00Z",
"eventType": "LENDER_DISPLAY_NAME_CHANGED",
"previousStatus": null,
"newStatus": null
}
]
}
```
**Event types**: `LENDER_REGISTERED` / `LENDER_STATUS_CHANGED` / `LENDER_DISPLAY_NAME_CHANGED` / `LENDER_JURISDICTION_CHANGED` / `LENDER_SIGNING_WALLET_CHANGED` / `LENDER_REGULATOR_REFERENCE_CHANGED` / `LENDER_CONTACT_NOTES_CHANGED`.
`previousStatus` / `newStatus` are non-null only for `LENDER_REGISTERED` + `LENDER_STATUS_CHANGED` events; both are null for metadata-change events.
**Use case**: An agent presenting a Schema B credit-risk attestation needs to confirm the issuing lender was in good standing at attestation time — walk the timeline, find the `LENDER_STATUS_CHANGED` events bracketing the attestation's `occurredAt`, and assert the lender was `ACTIVE` in that window (and not subsequently SUSPENDED / ARCHIVED).
Wraps: [`GET /v2/lenders/:lenderId/timeline`](/api-reference/public/lender-registry).
## `recommend_lender`
"Which lenders should this merchant approach?" — given a jurisdiction (+ optional archetype filter), returns the ordered list of ACTIVE lenders by exact-match-first, GLOBAL-fallback-second, track-record sort within group.
```typescript theme={null}
const recs = await mcp.callTool('recommend_lender', {
jurisdiction: 'AE',
archetype: 'fsra-licensed',
limit: 5,
});
```
```json theme={null}
{
"jurisdiction": "AE",
"count": 2,
"recommendations": [
{ "lenderId": "crediblex-uae", "matchKind": "exact-jurisdiction", "rank": 1 },
{ "lenderId": "valinor-vault", "matchKind": "global-fallback", "rank": 2 }
]
}
```
**Use case**: Merchant onboarding flow asking "which lenders should I apply to?" — agent surfaces exact-jurisdiction matches first (e.g. CredibleX for UAE merchants) with `GLOBAL` DeFi-vault fallback (e.g. Valinor) ranked second.
Wraps: [`GET /v2/lender-routing/recommend`](/api-reference/public/lender-routing).
## `recommend_service_provider`
"Which WMS partner should this merchant route to?" — same pattern as `recommend_lender` but for InventoryOS partners (WMS/3PL).
```typescript theme={null}
const recs = await mcp.callTool('recommend_service_provider', {
archetype: 'stord',
limit: 5,
});
```
```json theme={null}
{
"archetype": "stord",
"count": 1,
"recommendations": [
{
"providerId": "stord-us-east-1",
"displayName": "Stor'd US-East",
"successfulIngestionCount": 47,
"rank": 1
}
]
}
```
**Use case**: Merchant fulfillment onboarding asking "which 3PL should I integrate with" — agent ranks by track record (successful ingestion count + recency).
Wraps: [`GET /v2/service-provider-routing/recommend`](/api-reference/public/service-provider-routing).
## `get_methodology_versions`
Return all methodology document versions ever registered for a lender, newest-first. Verifiers use this to trace a lender's full methodology lineage when an on-chain Schema B attestation cites a specific hash.
```typescript theme={null}
const versions = await mcp.callTool('get_methodology_versions', { lenderId: 'crediblex-uae' });
```
```json theme={null}
{
"lenderId": "crediblex-uae",
"count": 3,
"versions": [
{
"version": 3,
"methodologyHash": "0xabc123...",
"documentUrl": "https://crediblex.ae/methodology/v3.pdf",
"displayName": "CredibleX Credit-Risk Methodology v3",
"status": "ACTIVE",
"effectiveAt": "2026-06-01T00:00:00Z",
"supersededAt": null
},
{
"version": 2,
"methodologyHash": "0xdef456...",
"documentUrl": "https://crediblex.ae/methodology/v2.pdf",
"displayName": "CredibleX Credit-Risk Methodology v2",
"status": "SUPERSEDED",
"effectiveAt": "2026-04-15T00:00:00Z",
"supersededAt": "2026-06-01T00:00:00Z"
},
{
"version": 1,
"methodologyHash": "0x789abc...",
"documentUrl": "https://crediblex.ae/methodology/v1.pdf",
"displayName": "CredibleX Credit-Risk Methodology v1",
"status": "SUPERSEDED",
"effectiveAt": "2026-03-01T00:00:00Z",
"supersededAt": "2026-04-15T00:00:00Z"
}
]
}
```
**Status enum**: `ACTIVE` / `SUPERSEDED` / `REVOKED`.
**Privacy + bounds**: no `notes` field is exposed (operator-only); response is hard-capped at 100 versions per lender.
**Use case**: An agent presented with a Schema B attestation needs to walk the lender's methodology history to see whether the cited `methodologyHash` is the most recent version or has been superseded — flag stale citations to the consuming application.
Wraps: [`GET /v2/methodologies/:lenderId/versions`](/api-reference/public/methodology-registry).
## `request_brand_attestation`
Queue a Schema A brand-attestation request on behalf of a merchant. The request walks `PENDING → APPROVED → MINTED` (or `→ REJECTED`); the on-chain mint is gated behind operator review — calling this tool does **not** directly mint.
```typescript theme={null}
const result = await mcp.callTool('request_brand_attestation', {
shopSlug: 'unstoppable',
notes: "Filed on behalf of merchant during agentic onboarding",
});
```
```json theme={null}
{
"requestId": "65f8a1b2c3d4e5f6a7b8c9aa",
"status": "PENDING",
"message": "Your request is in the operator review queue"
}
```
**Idempotent** on `(shopSlug, status: PENDING)` — re-calling while a `PENDING` row exists returns the same `requestId` instead of creating a duplicate. The agent never has to handle a "duplicate" 4xx.
**Use case**: an agent orchestrating a merchant's full onboarding flow — application form, lender match, brand-attestation request — needs to queue the brand-attestation request without the merchant clicking the shop-builder CTA. Pair with `get_brand_attestation_status` (next) to detect terminal state before handing back to the merchant.
Wraps: [`POST /v2/attestations/brand/:shopSlug/request`](/api-reference/public/brand-attestation-request). Full walkthrough at [Brand attestation: request → mint → verify](/guides/trust-fabric/brand-attestation-lifecycle).
## `get_brand_attestation_status`
Poll the brand-attestation request lifecycle. The status discriminator is one of `NONE` (no request exists), `PENDING`, `APPROVED`, `MINTED`, or `REJECTED`. The endpoint **always** returns 200; missing rows are reported as the synthetic `NONE` state so the agent never has to handle a 404.
```typescript theme={null}
const status = await mcp.callTool('get_brand_attestation_status', {
shopSlug: 'unstoppable',
});
```
```json theme={null}
{
"status": "MINTED",
"request": {
"requestId": "65f8a1b2c3d4e5f6a7b8c9aa",
"shopSlug": "unstoppable",
"status": "MINTED",
"attestationUid": "0x9c4f7a3e8b1d2c6f5a0b8e9d1c2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f",
"mintedAt": "2026-06-13T07:42:11Z",
"createdAt": "2026-06-12T18:00:00Z"
}
}
```
**Privacy**: operator-private fields (`decidedBy`, `decisionReason`, `mintError`, `mintAttempts`, `merchantId`, free-text `notes`) are **scrubbed** from this response. They only surface on the SUPER\_ADMIN admin route.
**Use case**: an agent loop walking a brand-attestation request from `PENDING → MINTED`. Recommended cadence: 30s → 60s → 120s exponential backoff for up to \~5 minutes, then hand the lifecycle back to the merchant. On `MINTED`, call `verify_brand_attestation` to confirm the on-chain envelope decodes cleanly and surface the easscan link to the merchant.
Wraps: [`GET /v2/attestations/brand/:shopSlug/request-status`](/api-reference/public/brand-attestation-request).
## `get_trust_fabric_stats`
"What's the platform's overall trust-fabric scale?" — returns aggregate-only counts across lenders, service providers, methodology versions, and attestations by schema. No PII, no per-row data, no auth required. Use this to gauge platform scale before issuing per-merchant queries or to power a partner-facing dashboard.
```typescript theme={null}
const stats = await mcp.callTool('get_trust_fabric_stats', {});
```
```json theme={null}
{
"asOf": "2026-06-13T02:00:00Z",
"lenders": { "total": 2, "active": 2, "pendingKyb": 0, "suspended": 0, "archived": 0 },
"serviceProviders": { "total": 5, "active": 4, "pendingKyb": 1, "suspended": 0, "archived": 0 },
"methodologies": { "totalLenders": 2, "activeVersions": 1, "supersededVersions": 3, "revokedVersions": 0 },
"attestations": { "schemaA": 12, "schemaB": 8, "schemaC": 4, "schemaD": 2 }
}
```
**Use case**: partner dashboards showing droplinked trust-fabric scale; agent platform-scale signaling; freshness/health probes.
Wraps: [`GET /v2/trust-fabric/stats`](/api-reference/public/trust-fabric-stats).
## Related
* [Brand attestation: request → mint → verify](/guides/trust-fabric/brand-attestation-lifecycle) — end-to-end Schema A lifecycle walkthrough
* [Trust Fabric overview](/concepts/trust-fabric) — full architecture
* [MCP Server](/agentic/mcp-server) — connection + auth
# MCP Server
Source: https://docs.droplinked.com/agentic/mcp-server
Droplinked's canonical MCP connector at mcp.droplinked.com — streamable HTTP transport, public discovery, two-tier auth, and how to connect Claude, ChatGPT, Cursor, and any MCP client.
Droplinked exposes its commerce primitives — catalog and merchant discovery, cart
composition, checkout, refunds, and trust-fabric reads — as **Model Context Protocol**
tools. The canonical surface is the standalone **droplinked-mcp connector at
`https://mcp.droplinked.com`** — the host submitted to the Anthropic Connectors Directory
and listed in the HOL registry via the [`droplinked/droplinked-codex-plugin`](https://github.com/droplinked/droplinked-codex-plugin)
bundle. Any MCP-capable agent discovers the surface from a public well-known doc and calls
it over streamable HTTP.
There are two MCP surfaces. **`mcp.droplinked.com`** (this page) is the canonical
connector — it's what you add to Claude, ChatGPT, or Cursor. The Droplinked backend also
serves an **always-on backend MCP surface** at `apiv3.droplinked.com/mcp/v1` — see
[Alternate surface](#alternate-surface-apiv3-backend-mcp) below. Prefer the connector.
For the full, code-grounded tool roster see [Inventory MCP](/agentic/inventory-mcp).
## Discovery
Start at the well-known discovery doc. It's **public** (no key), cacheable, and lists every
tool with its JSON-schema input shape:
```bash theme={null}
curl -s https://mcp.droplinked.com/.well-known/mcp.json | jq
```
```json theme={null}
{
"name": "droplinked-mcp",
"version": "1.0.0",
"protocolVersion": "2024-11-05",
"transport": "streamable-http",
"endpoints": {
"mcp": "/mcp",
"toolsList": "/mcp/tools",
"toolsCall": "/mcp/tools/{name}"
},
"capabilities": {
"tools": { "listChanged": false },
"resources": { "listChanged": false, "subscribe": false },
"prompts": false
},
"tools": [ { "name": "search_products", "description": "…" }, … ]
}
```
A **per-merchant** variant scopes the surface to one shop's catalog. Each shop advertises a
manifest and a pre-scoped tool list:
```bash theme={null}
# Per-merchant manifest (public)
curl -s https://mcp.droplinked.com/{shopSlug}/manifest | jq
# Per-merchant tool list, pre-bound to this shop
curl -s https://mcp.droplinked.com/{shopSlug}/mcp/tools
```
Health is at `GET /healthz`. See [Consume Droplinked MCP](/agentic/consume-droplinked-mcp)
for the full per-merchant pinning flow.
## Transport
The connector speaks **streamable HTTP** at `protocolVersion 2024-11-05`. There's one
JSON-RPC endpoint plus a REST-style shim for clients that prefer plain HTTP calls:
| Method + path | Purpose | Auth |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
| `POST /mcp` | The MCP lifecycle over JSON-RPC 2.0 on one endpoint — `initialize`, `notifications/initialized`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, `ping`. | Read tools open; write `tools/call` needs `X-MCP-API-Key`. |
| `GET /mcp/tools` | REST shim — list available tools (`{ tools: [...] }`). | **Public, no key.** |
| `POST /mcp/tools/{name}` | REST shim — invoke a tool by name with a flat `{ arguments }` body. | Read tools open; write tools need `X-MCP-API-Key`. |
| `GET /.well-known/mcp.json` | Discovery doc. | **Public, no key.** |
| `GET /healthz` | Liveness probe. | **Public, no key.** |
The JSON-RPC endpoint accepts the standard envelope; the REST shim carries the tool name in
the path and the arguments in a flat body. Errors use the MCP/JSON-RPC codes (`-32601`
unknown tool/method, `-32602` invalid arguments). A tool whose *logic* fails (e.g. shop not
found) still returns `200` with `isError: true` so the agent can recover.
```bash REST shim theme={null}
# List tools — public, no key
curl -s https://mcp.droplinked.com/mcp/tools | jq
# Call a read-only tool — public, no key
curl -s -X POST https://mcp.droplinked.com/mcp/tools/search_products \
-H 'content-type: application/json' \
-d '{ "query": "wool beanie" }'
```
```bash JSON-RPC theme={null}
curl -s -X POST https://mcp.droplinked.com/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "search_products", "arguments": { "query": "wool beanie" } }
}'
```
## Authentication — two tiers (live)
The connector runs a **two-tier auth model, live in production as of 2026-08-05**. It is not
a "later phase" and it is not fully open.
### Read-only tools are public — no key
Every discovery, catalog, trust, verify, and recommend tool is callable with **no
credential**. `GET /mcp/tools` and the read side of `tools/call` are open, so Anthropic,
ChatGPT, Cursor, and any MCP client can list and call them with zero setup:
`find_merchant` · `find_inventory` · `search_products` · `get_product` ·
`list_shop_products` · `verify_brand_attestation` · `get_trust_dossier` ·
`recommend_lender` — and the rest of the read-only catalog/trust surface.
### Write tools require `X-MCP-API-Key`
State-changing tools return **`401` without a key** and require the `X-MCP-API-Key` header:
`process_payment` · `start_checkout` · `cart.addLine` · `cart.updateLineQuantity` ·
`cart.removeLine` · `cart.applyDiscount` · `quote_inventory_available` ·
`request_partner_referral` · `quote_credit_terms` · `report_repayment` ·
`request_brand_attestation`.
Request a key from **[ops@droplinked.com](mailto:ops@droplinked.com)**. Treat it like an API secret — it identifies your
agent for attribution, rate-limiting, and the commission rail.
```bash theme={null}
# Write tool — 401 without the key
curl -s -X POST https://mcp.droplinked.com/mcp/tools/start_checkout \
-H 'content-type: application/json' \
-H "X-MCP-API-Key: $DROPLINKED_MCP_KEY" \
-d '{ "shopUrl": "warmwool", "items": [{ "skuId": "65f…", "quantity": 1 }] }'
```
### Two-header identity model
* **`X-MCP-API-Key`** authorizes the *write request to the server* — it is what lifts the
`401` on write tools.
* **`Authorization: Bearer `** (optional) carries the *consumer agent's identity*
for tools that require an authenticated caller.
Send both when a write tool needs the calling agent's identity; send just `X-MCP-API-Key`
for writes that don't. **Rate-limiting applies to all routes**, keyed or not.
## Registry + one-click add
The connector is packaged for one-click add through the
[`droplinked/droplinked-codex-plugin`](https://github.com/droplinked/droplinked-codex-plugin)
bundle — the HOL registry listing. Agents (and the Anthropic Connectors Directory) can pull
the plugin to register `mcp.droplinked.com` without hand-wiring the transport.
## Connect from an agent
### Claude / Claude Code
Point Claude's remote connector at the single streamable-HTTP endpoint. The client drives
the `initialize` handshake and negotiates `protocolVersion 2024-11-05` automatically:
```bash theme={null}
claude mcp add --transport http droplinked https://mcp.droplinked.com/mcp
```
The client lists tools via `tools/list` and dispatches calls via `tools/call`. **Read-only
tools work with no key.** To call write tools (checkout, payments, cart mutations), add your
`X-MCP-API-Key` as a header on the connector.
### ChatGPT / OpenAI Agents SDK
Add `https://mcp.droplinked.com/mcp` as an MCP tool source, or ingest the
[ACP feed](/agentic/acp-feed) for product discovery. The read-only tool surface and the ACP
feed are both public.
### Any MCP client (manual)
1. `GET /.well-known/mcp.json` to learn the tool surface (public).
2. `GET /mcp/tools` — or `POST /mcp` with `tools/list` — to enumerate tools + input schemas
(public).
3. Invoke: `POST /mcp/tools/{name}` with `{ arguments }`, or `POST /mcp` with a `tools/call`
envelope. Read tools need no key; write tools need `X-MCP-API-Key`.
## How it fits together
```
AI agent (Claude / ChatGPT / Cursor / Agents SDK)
│ GET /.well-known/mcp.json (discover — public)
│ GET /mcp/tools (list — public, no key)
│ POST /mcp (JSON-RPC: initialize / tools/list / tools/call)
│ POST /mcp/tools/{name} (REST call — writes need X-MCP-API-Key)
▼
droplinked-mcp connector (mcp.droplinked.com) · two-tier auth
│ read-only tools open · write tools keyed (401 without X-MCP-API-Key)
▼
Droplinked backend (apiv3) — McpToolRegistryService
│ catalog / cart / order / trust-fabric reads + writes
▼
PSP resolver (Stripe · PayPal · Paymob · MamoPay · x402 · …) · EAS trust fabric
▼
recordAgenticIntent → attribution ledger → 70 / 20 / 10 settlement
```
## Alternate surface: apiv3 backend MCP
The Droplinked backend serves an **always-on backend MCP surface** directly at
`apiv3.droplinked.com/mcp/v1`, over four JSON-RPC routes. It's the embedded surface the
connector proxies to — useful if you're integrating against the backend host directly rather
than through the canonical connector.
| Method + path | Purpose |
| ----------------------------- | ---------------------------------------------- |
| `POST /mcp/v1/tools/list` | List available tools (`{ tools: [...] }`). |
| `POST /mcp/v1/tools/call` | Invoke a tool by name. |
| `POST /mcp/v1/resources/list` | List MCP resources. |
| `POST /mcp/v1/resources/read` | Read a resource by `mcp://droplinked/...` URI. |
Each route accepts **both** request shapes — a flat body
(`{ "name": "searchProducts", "arguments": { … } }`) or a JSON-RPC 2.0 envelope
(`{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { … } }`) — and mirrors the
shape back in the response.
```bash theme={null}
curl -s -X POST https://apiv3.droplinked.com/mcp/v1/tools/call \
-H 'content-type: application/json' \
-d '{ "name": "searchProducts", "arguments": { "query": "wool beanie" } }'
```
* **Discovery** for this surface is at
`GET https://apiv3.droplinked.com/.well-known/mcp.json`.
* **Write-tool gating** on this surface is being **aligned to the connector's two-tier
model** — treat write tools here as gated, and use the canonical connector's
`X-MCP-API-Key` model as the reference behavior.
* **Admin diagnostics** (`queryEventLog`, `traceAttestationLineage`, `getInventoryDrift`)
are gated by an `_adminKey` *argument* matching `MCP_ADMIN_SECRET` — not a header. This is
scoped to the backend surface's diagnostic tools only.
New integrations should target `mcp.droplinked.com`. The apiv3 surface stays in place as a
backend-embedded MCP, but the canonical connector is the discovery-directory listing and
the one that carries the live two-tier auth model.
## Related
The full tool registry, read/write annotations, and `findInventory` details.
Build an agentic shopping loop end-to-end against `mcp.droplinked.com`.
The Stripe Agentic Commerce Protocol product feed for shopping-surface ingestion.
How agentic conversions settle 70 / 20 / 10.
# Merchant Discovery Rules
Source: https://docs.droplinked.com/agentic/merchant-discovery-rules
How agentic discovery commission is calculated and how KYB verification gates merchant visibility across MCP tools.
Two operator-locked decisions govern what agents see when they discover merchants and how
commission is attributed when an agentic sale closes. This page documents both rules so
agent developers and merchants can reason about incentives and visibility without surprises.
***
## How agentic discovery commission is calculated
When an AI agent surfaces a Droplinked merchant product and the buyer completes a purchase,
a **discovery commission** is deducted from the platform's 20% share and paid to the
agent's affiliate-of-record. The commission rate scales with the merchant's monthly agentic
GMV — rewarding high-volume merchants with a lower effective rate.
### Commission rate table
| Monthly agentic GMV (per merchant) | Discovery commission rate |
| ---------------------------------- | ---------------------------------------- |
| Up to \$10,000 | 5% |
| $10,001 – $49,999 | Linear interpolation (see formula below) |
| \$50,000 and above | 3% |
The rate applies to the merchant's trailing-30-day agentic GMV, computed at settlement time.
### Linear interpolation formula
For GMV between $10K and $50K, the rate interpolates linearly between 5% and 3%:
```
rate = 5% - ((GMV - 10_000) / (50_000 - 10_000)) * (5% - 3%)
= 5% - ((GMV - 10_000) / 40_000) * 2%
```
At $30K GMV this yields exactly 4%. At $10K it yields 5%; at \$50K it yields 3%.
### Kill-switch override
Operators can override the computed rate platform-wide using the environment variable:
```bash theme={null}
AGENTIC_DISCOVERY_COMMISSION_RATE=0.04 # fix at 4% regardless of GMV
```
When set, this value supersedes the tier table for all merchants. Remove the variable to
restore the tiered formula.
### Storyflo-mirror affiliate-of-record (Phase 2)
In Phase 1, the discovering agent's platform identity is the affiliate-of-record and
receives the commission. **Phase 2** (planned) will honour a referring affiliate: when a
Storyflo-mirrored affiliate installs the agent that makes the sale, that affiliate earns
the commission instead of the agent platform. This referral chain is not yet live —
tracking in [droplinked-backend#2086](https://github.com/droplinked/droplinked-backend/issues/2086).
### Tracking
* Decision #2 in [droplinked-backend#2091](https://github.com/droplinked/droplinked-backend/issues/2091)
* Implementation: [#2094](https://github.com/droplinked/droplinked-backend/issues/2094) (commission calculator) + [#2086](https://github.com/droplinked/droplinked-backend/issues/2086) (affiliate-of-record)
***
## How KYB verification gates visibility
Droplinked uses the [Schema A attestation chain](/concepts/trust-fabric) as the canonical
signal for KYB (Know Your Business) verification. A merchant is considered **verified**
once their Schema A attestation chain is complete. Unverified merchants are not absent from
the platform — they can still list products, process orders, and receive payouts — but they
surface differently (or not at all) through the agentic discovery layer.
### Visibility per MCP tool
| Tool | Verified merchants | Unverified merchants |
| --------------------------- | -------------------------------- | ------------------------------------------------ |
| `searchMerchants` | Returned in results | Not returned — excluded at query time |
| `getMerchantCatalogSummary` | Returns full catalog summary | Returns `null`; envelope carries `isError: true` |
| `getMerchantDirectory` | Included; `verifiedTier: "full"` | Included; `verifiedTier: "directory_only"` |
**`searchMerchants`** is the primary discovery surface for agents that need to find a
merchant by name, category, or keyword. Only KYB-verified merchants appear here — so an
unverified merchant is effectively invisible to an agent running a discovery search.
**`getMerchantCatalogSummary`** is called after a merchant is already known to the agent
(e.g. the agent has a shop slug). For an unverified merchant the envelope is:
```json theme={null}
{
"isError": true,
"data": null,
"reason": "merchant_not_verified"
}
```
**`getMerchantDirectory`** exposes both tiers so directory-style UIs can show the full
platform roster. Agents should inspect `verifiedTier` before presenting a merchant as
fully trust-attested:
```json theme={null}
{
"merchants": [
{ "shopSlug": "acme-store", "displayName": "Acme Store", "verifiedTier": "full" },
{ "shopSlug": "new-shop", "displayName": "New Shop", "verifiedTier": "directory_only" }
]
}
```
### `verifiedTier` values
| Value | Meaning |
| ------------------ | -------------------------------------------------------------- |
| `"full"` | Schema A attestation chain complete — merchant is KYB-verified |
| `"directory_only"` | Merchant is registered but Schema A chain is incomplete |
### Schema A attestation requirement
KYB verification is anchored to **EAS Schema A** on Base mainnet. Completion requires the
full 4-step attestation chain:
1. Entity registration (Schema A)
2. Attestation issued by an authorised issuer wallet
3. Issuer active in the `EasIssuer` registry at issuance time
4. Attestation not revoked
Agents can verify the chain independently using the `verify_lender` / trust-fabric tools or
by reading Schema A UIDs directly from the Base mainnet EAS explorer.
### Upsell path for merchants
Merchants that want to appear in `searchMerchants` results need to complete KYB. From the
Droplinked dashboard, navigate to **Settings → Verification** and follow the Schema A
attestation flow. Once the chain is confirmed on-chain, the visibility change takes effect
on the next platform index refresh (typically within minutes).
### Tracking
* Decision #4 in [droplinked-backend#2091](https://github.com/droplinked/droplinked-backend/issues/2091)
* Implementation: [#2096](https://github.com/droplinked/droplinked-backend/issues/2096) (`searchMerchants` + `getMerchantCatalogSummary` gates) + [#2099](https://github.com/droplinked/droplinked-backend/issues/2099) (`getMerchantDirectory` + `verifiedTier` enum)
***
## Related
* [Inventory MCP](/agentic/inventory-mcp) — full tool inventory, including `listMerchants`
and catalog discovery tools
* [MCP Server](/agentic/mcp-server) — installation and environment reference
* [Trust fabric](/concepts/trust-fabric) — Schema A / B / C / D EAS trust chain overview
* [Connect your store](/agentic/connect-your-store) — onboard a merchant and start the
Schema A verification flow
* [Lender Trinity MCP Tools](/agentic/lender-trinity-mcp-tools) — trust-fabric agent tools
that complement merchant discovery
# Agentic Commerce
Source: https://docs.droplinked.com/agentic/overview
Make a merchant's existing inventory shoppable by AI agents — MCP, the Stripe ACP feed, and x402 micropayments.
Droplinked meets merchants **where they already publish their inventory** and projects it into
agentic surfaces — so an AI agent (ChatGPT, Claude, Cursor, and others) can discover a product,
start a purchase, and route affiliate revenue, without the merchant rebuilding anything.
## Building blocks
The Droplinked backend serves a 25-tool Model Context Protocol surface over HTTP — catalog
search, cart composition, order placement, refunds, and trust-fabric reads. Discover it at
`/.well-known/mcp.json` and call it from any MCP client.
A Stripe **Agentic Commerce Protocol** product feed at
`https://apiv3.droplinked.com/feed/acp.json` — the inventory surface agentic shopping
consumes.
Pay-per-call settlement + the 70/20/10 affiliate revenue split, so agents (and the
publishers that route them) earn on every conversion.
## MCP tools
The MCP server exposes **25 tools** spanning catalog discovery, cart + order, merchant
discovery, and trust + financing. A few of the most-used:
| Tool | Purpose |
| --------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `searchProducts` | Free-text search across the public Droplinked catalog |
| `findInventory` | SKU-availability-aware multi-source query (native + live `impact_brand`) for cart composition + multi-brand discovery |
| `getProductDetail` | Full product detail for a `shopUrl` + `productSlug` |
| `placeOrderAgentic` | Compose an order and optionally resolve the payment-intent in one call |
| `recordAgenticIntent` | Log an external agent's intent against an opted-in merchant (feeds attribution) |
Tools are served over HTTP at `https://apiv3.droplinked.com` (`/mcp/v1/tools/{list,call}`),
accept both a flat body and a JSON-RPC 2.0 envelope, and are discoverable from
`/.well-known/mcp.json`. See [Inventory MCP](/agentic/inventory-mcp) for the full roster and
[MCP Server](/agentic/mcp-server) for the transport.
## The merchant story
A merchant who already sells on Droplinked (or connects an existing store) gets agentic
distribution for free: their catalog is published to the ACP feed and the MCP server, agents
surface it to buyers, and the affiliate engine attributes and splits the revenue. This is the
on-chain, agent-native distribution layer on top of the standard [commerce APIs](/api-reference/introduction).
This section is being expanded with end-to-end integration guides (install the MCP server,
register your inventory to the ACP feed, and close the agent purchase loop).
## Integration shortcuts
This docs site exposes several agent-friendly entry points out of the box:
| Surface | URL | Use |
| --------------- | -------------------------------------------- | --------------------------------------------------------------------------------- |
| `llms.txt` | `https://docs.droplinked.com/llms.txt` | Compact index of every page in this site, for LLM context loading |
| `llms-full.txt` | `https://docs.droplinked.com/llms-full.txt` | Full Markdown corpus for retrieval/embedding |
| OpenAPI spec | `https://apiv3.droplinked.com/swagger/json` | Machine-readable spec backing the API Reference tab |
| ACP feed | `https://apiv3.droplinked.com/feed/acp.json` | Stripe Agentic Commerce Protocol product feed |
| `/health` | `https://apiv3.droplinked.com/health` | JSON health for the public API (see the [API Status page](/api-reference/status)) |
Every page in this site also surfaces a **contextual toolbar** with one-click
"open in Claude / open in ChatGPT / copy as Markdown / add as MCP server"
shortcuts — handy when wiring this documentation into an agent's context.
# Storefront MCP Discovery
Source: https://docs.droplinked.com/agentic/storefront-mcp-discovery
Make your storefront agent-shoppable by advertising your per-merchant MCP URL via a `` tag. ChatGPT, Claude, Cursor, Continue, Cline, and Vercel AI SDK convergently probe for this.
The MCP server at `mcp.droplinked.com` already serves a per-merchant tool surface at
`/{shopSlug}/manifest`, `/{shopSlug}/mcp/tools`, and `/{shopSlug}/mcp/tools/{name}` — but an
agent that arrives at your storefront in the wild has no signal pointing it at that surface.
This page closes that gap.
The convergent pattern across ChatGPT, Claude, Cursor, Continue, Cline, and the Vercel AI
SDK is the same: probe the storefront `` for a `` tag. Add one
line of HTML and your storefront becomes agent-shoppable — additive distribution, not
migration. Your existing checkout, catalog, and merchant config are untouched.
## The pattern
```html theme={null}
```
That's the whole contract. Drop it in your storefront's `` and agents will discover
your per-merchant MCP surface on the next page load.
## What changes
* An agent given only your storefront URL can probe the HTML, discover your MCP URL, fetch
your manifest, and call your tools — no prior knowledge of Droplinked required.
* No additional auth is required. The per-merchant surface is unauthenticated; merchants
opt in by registering the shop on Droplinked.
* Your catalog becomes discoverable by agentic shopping surfaces — Stripe ACP, OpenAI
ChatGPT, Claude.ai, and any MCP-capable client.
## Implementation by storefront type
Storefronts hosted on `droplinked.io/{shopSlug}` advertise the meta tag automatically.
No action needed — the storefront template injects it at render time using your shop
slug.
Verify with:
```bash theme={null}
curl -s https://droplinked.io/your-shop-slug | grep -i 'mcp-url'
```
Add to your root layout or `` component:
```tsx theme={null}
// app/layout.tsx (Next.js App Router)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
Or via `next/head` in the Pages Router:
```tsx theme={null}
import Head from 'next/head';
export default function Storefront() {
return (
<>
{/* ... */}
>
);
}
```
Add to `layout/theme.liquid` inside the `` block:
```liquid theme={null}
{% comment %} ... existing head tags ... {% endcomment %}
```
If you advertise multiple Droplinked-connected shops from one Shopify theme, pick the
primary slug — agents follow the first `mcp-url` they see.
Paste the tag into your storefront's ``:
```html theme={null}
```
## Discovery walkthrough
The agent gets your storefront URL from a user prompt, a search result, a link in chat,
or a referrer. It knows nothing else about your shop.
The agent issues a `GET` for the storefront page and scans the `` for
``. The `content` attribute is your per-merchant MCP base URL.
```bash theme={null}
curl https://mcp.droplinked.com/your-shop-slug/manifest
```
The manifest declares shop metadata, tool URLs, and any auth requirements (none, for
the public catalog surface).
```bash theme={null}
curl https://mcp.droplinked.com/your-shop-slug/mcp/tools
```
Returns the same tool inventory as the platform-wide [MCP server](/agentic/mcp-server),
pre-bound to your shop — no shop-slug argument required on every call.
```bash theme={null}
curl -X POST https://mcp.droplinked.com/your-shop-slug/mcp/tools/search_products \
-H 'content-type: application/json' \
-d '{"query": "t-shirt"}'
```
From here the agent runs the usual MCP loop — `search_products`, `get_product`,
`start_checkout` — scoped to your catalog.
## Validation
Verify your storefront emits the meta tag:
```bash theme={null}
curl -s https://your-shop.example.com/ | grep -i 'mcp-url'
```
You should see a single line containing ``.
Verify the per-merchant manifest responds:
```bash theme={null}
curl https://mcp.droplinked.com/your-shop-slug/manifest
```
You should get a JSON document with your shop metadata and the tool URLs. A 404 means the
shop slug isn't registered — see [Connect your store](/agentic/connect-your-store).
## Fallback discovery
If your storefront can't add the meta tag (e.g. a hosted platform with no `` editor),
agents can still find your shop via:
* **Platform-wide discovery** — the well-known doc at
`https://mcp.droplinked.com/.well-known/mcp.json` carries a
`merchantDiscovery.pathTemplate` an agent can substitute any shop slug into.
* **The `find_merchant` tool** — exposed on the platform-wide [MCP server](/agentic/mcp-server),
searchable by slug, name, or category.
* **The ACP feed** — `https://apiv3.droplinked.com/feed/acp.json` lists every Droplinked
shop's catalog and is ingested by Stripe ACP and other agentic shopping surfaces.
Storefront-advertised discovery is the lowest-friction path for an agent that lands on a
single shop; the fallbacks cover everything else.
## Privacy & security
The per-merchant manifest exposes only catalog metadata (shop name, slug, product feed
URL, tool URLs) and payment-method categories. It does **not** expose PII, merchant
operator data, sales numbers, or internal config. Merchants can opt out by un-registering
the shop (operator-gated).
## Related
* [MCP server](/agentic/mcp-server) — platform-wide tool inventory inherited by every
per-merchant surface.
* [Connect your store](/agentic/connect-your-store) — register a shop so its per-merchant
surface comes online.
* [Lender trinity MCP tools](/agentic/lender-trinity-mcp-tools) — agent-facing trust-fabric
tools that pair with per-merchant discovery.
* [Platform model](/concepts/platform-model) — how the agentic layer sits atop the commerce
primitives.
# Underwriting MCP Tools
Source: https://docs.droplinked.com/agentic/underwriting-mcp-tools
Composite read tools for lender-agent / merchant-portal flows: get_underwriting_signals, get_upgrade_preview, verify_methodology.
Three MCP tools wrapping the underwriting layer of the [trust fabric](/concepts/trust-fabric). Together they cover the "should I underwrite + at what tier" decision in one round trip, the merchant-facing aspirational roadmap, and the forensic-chain step that audits the underwriting methodology cited on a Schema B attestation.
## `get_underwriting_signals`
Composite envelope that bundles into ONE call what previously took 3-4 separate per-axis verifier calls: Schema B latest-per-lender + Schema C merchant-wide rollup + CreditTier upgrade preview + a `summary` block.
```typescript theme={null}
const signals = await mcp.callTool('get_underwriting_signals', {
merchantId: '6a207db0d29923bffaa983ca',
});
```
```json theme={null}
{
"merchantId": "6a207db0d29923bffaa983ca",
"creditRisk": {
"hasAny": true,
"activeCount": 2,
"maxObservedTier": "T3",
"latestPerLender": [
{ "lenderId": "crediblex-uae", "creditTier": "T2",
"maxCreditLineUsdCents": 5000000,
"lenderCurrentStatus": "ACTIVE", "status": "ACTIVE", ... },
{ "lenderId": "valinor-vault", "creditTier": "T3",
"maxCreditLineUsdCents": 10000000,
"lenderCurrentStatus": "SUSPENDED", "status": "ACTIVE", ... }
]
},
"repaymentHistory": {
"hasAny": true,
"perLenderCount": 2,
"merchantWide": {
"totalSettlements": 17, "totalOnTime": 13,
"totalLate": 3, "totalDefaults": 1,
"trailingTwelveMonthDefaults": 1,
"mostRecentSettlementAt": "2026-06-05T..."
}
},
"upgradeEligibility": {
"observedTier": "T2",
"nextTierTarget": "T3",
"onTimeSettlementsNeeded": 8,
"blockingDefaultCount": 1
},
"summary": {
"anchorTier": "T3",
"totalActiveCreditLineUsdCents": 15000000,
"reliabilityScore": 76
}
}
```
### Watch the `summary` block — it's load-bearing
| Field | Why it matters |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `anchorTier` | `max(observed-from-repayment, already-issued)` — a lender never under-prices a merchant who has already proven a higher tier on a different lender's book |
| `totalActiveCreditLineUsdCents` | Sum of `maxCreditLineUsdCents` across all ACTIVE Schema B attestations |
| `reliabilityScore` | `onTimeSettlements / totalSettlements * 100` (0-100); `null` when no settlement history yet |
### Watch `latestPerLender[].lenderCurrentStatus`
When the attestation `status: ACTIVE` but `lenderCurrentStatus: SUSPENDED` / `ARCHIVED` / `UNKNOWN`, the on-chain attestation is still valid but the issuer has been de-listed in the LenderRegistry. **Verifier-side policy decides** whether to honor.
Wraps: [`GET /v2/underwriting-signals/:merchantId`](/api-reference/public/underwriting-signals).
## `get_upgrade_preview`
Aspirational roadmap to higher credit-tier ceilings. Use this on merchant-portal flows asking "what does it take to climb?"
```typescript theme={null}
const preview = await mcp.callTool('get_upgrade_preview', {
merchantId: '6a207db0d29923bffaa983ca',
});
```
```json theme={null}
{
"observedTier": "T1",
"nextTierGap": {
"targetTier": "T2",
"onTimeSettlementsNeeded": 1,
"blockingDefaultCount": 0,
"guidance": "Settle 1 more on-time to reach T2 ceiling."
},
"topTierGap": {
"targetTier": "T3",
"onTimeSettlementsNeeded": 8,
"blockingDefaultCount": 0,
"guidance": "Settle 8 more on-time to reach T3 (top ceiling)."
}
}
```
**Aspirational, not a promise.** The actual issued tier depends on the lender's base tier mapping (revenue + inventory + sales-efficiency signals). This tool shows the *floor* a merchant can earn through repayment performance.
Tier ladder:
| Tier | Required | Blocks |
| ---- | ---------------------------- | -------------------------- |
| `T1` | Default — every new merchant | — |
| `T2` | 3+ on-time settlements | Lifetime defaults > 0 |
| `T3` | 10+ on-time settlements | Trailing-12mo defaults > 0 |
When the merchant reaches T3 both `nextTierGap` and `topTierGap` are `null`.
Wraps: [`GET /v2/merchant/credit-tier/upgrade-preview/:merchantId`](/api-reference/public/upgrade-preview).
## `verify_methodology`
Look up a lender's underwriting methodology. Two modes:
```typescript theme={null}
// Active mode — no methodologyHash
const active = await mcp.callTool('verify_methodology', {
lenderId: 'crediblex-uae',
});
// Hash-lookup mode — supplied methodologyHash
const cited = await mcp.callTool('verify_methodology', {
lenderId: 'crediblex-uae',
methodologyHash: '0xabc...',
});
```
```json theme={null}
{
"found": true,
"lenderId": "crediblex-uae",
"version": "2026.06.1",
"methodologyHash": "0xabc...",
"documentUrl": "https://crediblex.example.com/methodology-2026-06-1.pdf",
"displayName": "CredibleX Inventory-Financing v2",
"status": "ACTIVE",
"effectiveAt": "2026-06-01T00:00:00Z",
"supersededAt": null
}
```
**Use case**: a verifier consuming a Schema B credit-risk attestation reads the cited `methodologyHash` from the on-chain payload, calls this tool with that hash, downloads `documentUrl`, hashes it themselves, and compares. Any divergence flags methodology tampering.
Status enum:
| Status | Meaning |
| ------------ | ----------------------------------------------------------------------------------- |
| `ACTIVE` | The currently-effective methodology — what new Schema B mints reference |
| `SUPERSEDED` | An older version cited on legacy attestations; the lender has since updated |
| `REVOKED` | Operator pulled this methodology; existing attestations are flagged for re-issuance |
Wraps: `GET /v2/methodologies/:lenderId/active` and `GET /v2/methodologies/:lenderId/:methodologyHash`.
## Related
* [Trust Fabric overview](/concepts/trust-fabric) — full architecture
* [Lender Trinity MCP Tools](/agentic/lender-trinity-mcp-tools) — `verify_lender`, `recommend_lender`, `recommend_service_provider`
* [Forensic Chain Workflow](/concepts/forensic-chain) — how the tools compose for end-to-end attestation audit
# x402 Premium ACP Feed
Source: https://docs.droplinked.com/agentic/x402-premium-feed
How agents pay for premium Droplinked discovery feeds using the x402 HTTP micropayment protocol on Base mainnet (USDC).
The standard ACP feed at `/feed/acp.json` is open and free — every catalog item Droplinked
exposes to agentic shoppers lives there. The **premium** feed at `/feed/acp.premium.json`
returns a richer, ranked, agent-tuned slice of the same inventory (higher refresh cadence,
attribution-weighted ordering, more granular merchant signals) — and it is **gated by the
x402 HTTP 402 micropayment protocol**.
The price is intentionally **per-call** rather than per-subscription so an autonomous agent
can pull the feed on demand without operating a Droplinked subscription.
***
## At a glance
| | |
| ------------------ | ------------------------------------------------------------------ |
| **Endpoint** | `GET https://apiv3.droplinked.com/feed/acp.premium.json` |
| **Protocol** | [x402](https://x402.org) — `402 Payment Required` per call |
| **Settlement** | USDC on Base mainnet |
| **Price per call** | \$0.001 (1,000 atomic USDC units, 6 decimals) |
| **Facilitator** | `https://x402.org/facilitator` (Coinbase canonical) |
| **PayTo** | `0xB2721aD74B8E88F8c31f61c88c42b41468f5ba28` (Droplinked treasury) |
| **Asset** | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` (Circle USDC on Base) |
***
## The protocol in three round-trips
```text theme={null}
1. Agent: GET /feed/acp.premium.json (no payment)
Droplinked: 402 Payment Required
Body: {
"scheme": "exact",
"network": "base",
"maxAmountRequired": "1000",
"asset": "0x833589f…",
"payTo": "0xB2721aD7…",
"resource": "https://apiv3.droplinked.com/feed/acp.premium.json",
"extra": { "currency": "USDC" }
}
2. Agent (off-protocol): signs a USDC transfer for 1,000 atomic units to `payTo`
on Base mainnet, gets back a payment proof.
3. Agent: GET /feed/acp.premium.json
Header: X-PAYMENT:
Droplinked: facilitator verifies proof → 200 OK + JSON feed body
```
The first call always returns `402` — there is no API key authentication. Every authorised
caller is identified by a signed payment proof.
***
## How agents typically integrate this
Most agent SDKs that target Coinbase / Anthropic / OpenAI surfaces ship with x402 support
out of the box. The pattern is:
1. The agent has a **CDP (Coinbase Developer Platform) wallet** or another EIP-1559 wallet
with USDC on Base.
2. The wallet is configured with a per-request signing budget (e.g. "spend up to \$1 USDC
per minute on x402-gated endpoints").
3. The agent's HTTP client transparently retries the second-round GET with the `X-PAYMENT`
header when it sees a 402, using cached proofs to avoid re-signing within a quote
window.
If you're building an agent without SDK support, you can implement the full handshake by
reading the [x402 protocol spec](https://x402.org).
***
## What's in the premium feed
The premium feed body is shape-compatible with `/feed/acp.json` so agents that already
consume the open feed can swap endpoints. The differences:
* **Refresh cadence**: \~5 minutes (open feed is hourly)
* **Ranking**: attribution-weighted — merchants with verified KYB (`verifiedTier: "full"`),
active EAS Schema A chain, and >\$10K monthly agentic GMV bubble up
* **Granular signals**: each catalog row carries `discoveryRank`, `verifiedTier`,
`monthlyAgenticGmvBucket`, and `lastSettledOrderAt`
* **No throttling**: the open feed limits requests per IP; the premium feed has no
IP-based throttle since each call is paid
***
## Calibration window (current)
The premium feed is **live on production** as of 2026-06-15 02:18Z. Per Droplinked operator
decision `#2097`, on-chain USDC settlement against the treasury wallet starts firing once
the trailing-30-day accrual log crosses **\$1,000 USDC pending**. Until then, the protocol
captures + verifies payments via the facilitator but routes the settlement event to the
internal accrual log (`AffiliatePayoutAccrual`) rather than to an on-chain transfer.
This is a deliberate ramp: the protocol gating is real (agents must sign and present
proofs), but the settlement-to-treasury leg flips when the volume justifies on-chain gas.
Operators can verify the current accrual state via the SuperAdmin dashboard at
`https://admin.droplinked.com/monetization/x402-earnings`.
***
## Failure modes
| HTTP status | Reason |
| ------------------------------------ | ------------------------------------------------------------------------------- |
| `402` | First call — present a signed payment proof in `X-PAYMENT` and retry. |
| `503` `X402_NOT_CONFIGURED` | Operator has not set `X402_FACILITATOR_URL`. Surface to operator, do not retry. |
| `503` `X402_FACILITATOR_UNAVAILABLE` | Facilitator circuit breaker is OPEN. Retry with exponential backoff. |
| `4xx` after `X-PAYMENT` | Payment proof rejected — re-sign with a fresh nonce. |
| `200 OK` | Body is the premium feed JSON. |
***
## Why x402 (and not API keys)
x402 was chosen over API keys for the same reason Droplinked is built on EAS attestations:
**the buyer/agent identity is the cryptographic signature**, not a long-lived shared secret
held by an account team. There's no key to rotate, no key to leak, no quota to provision.
An agent operator pays for the calls they actually make, settles in USDC on-chain, and gets
no recurring bill.
The trade-off is that agent wallets need USDC on Base (\$0.001 per call is trivial but
non-zero), and the protocol is younger than HTTP Basic. Both gaps are being closed by the
broader x402 ecosystem — Storyflo's `get_premium_briefing` MCP tool uses the exact same
facilitator and settlement pattern as a reference integration.
***
## Related
* [ACP feed](/agentic/acp-feed) — the open, unauthenticated version
* [Storefront MCP discovery](/agentic/storefront-mcp-discovery) — tool-level discovery for
agents that don't want to ingest a bulk feed
* [Merchant Discovery Rules](/agentic/merchant-discovery-rules) — how KYB verification
gates merchant visibility (which in turn drives premium-feed ranking)
* [x402 protocol](https://x402.org) — external protocol spec
# Create a new address book entry (JWT required)
Source: https://docs.droplinked.com/api-reference/address-book/create-a-new-address-book-entry-jwt-required
https://apiv3.droplinked.com/swagger/json post /address-book
Route: AddressBooksService.createAddressBook
# Create address book for anonymous customer (public)
Source: https://docs.droplinked.com/api-reference/address-book/create-address-book-for-anonymous-customer-public
https://apiv3.droplinked.com/swagger/json post /address-book/public/anonymous-customer
Route: AddressBooksService.createAddressBook
# Delete address book by ID (JWT required)
Source: https://docs.droplinked.com/api-reference/address-book/delete-address-book-by-id-jwt-required
https://apiv3.droplinked.com/swagger/json delete /address-book/{id}
Route: AddressBooksService.deleteAddressBooks
# Get address book by ID (JWT required)
Source: https://docs.droplinked.com/api-reference/address-book/get-address-book-by-id-jwt-required
https://apiv3.droplinked.com/swagger/json get /address-book/{id}
Route: AddressBooksService.getAddressBook
# Get all address books for authenticated user (JWT required)
Source: https://docs.droplinked.com/api-reference/address-book/get-all-address-books-for-authenticated-user-jwt-required
https://apiv3.droplinked.com/swagger/json get /address-book
Route: AddressBooksService.getAddressBooks
# Get shipping address books (JWT required)
Source: https://docs.droplinked.com/api-reference/address-book/get-shipping-address-books-jwt-required
https://apiv3.droplinked.com/swagger/json get /address-book/shipping
Route: AddressBooksService.getAddressBooks
# Get shop address books (JWT required, PRODUCER/ADMIN role)
Source: https://docs.droplinked.com/api-reference/address-book/get-shop-address-books-jwt-required-produceradmin-role
https://apiv3.droplinked.com/swagger/json get /address-book/shop
Route: AddressBooksService.getShopAddressBook
# Update address book by ID (JWT required)
Source: https://docs.droplinked.com/api-reference/address-book/update-address-book-by-id-jwt-required
https://apiv3.droplinked.com/swagger/json put /address-book/{id}
Route: AddressBooksService.updateAddressBooks
# Aggregate Merchant Provisioner
Source: https://docs.droplinked.com/api-reference/admin/aggregate-provisioner
Single + bulk merchant provisioning with partnership-PSP preset routing — and a dry-run preview.
The aggregate merchant provisioner accepts merchant payloads, applies the
**partnership-PSP preset** for the merchant's region / cohort, and persists the merchant
with PSP configuration already wired. This eliminates the historical 3-step dance
(create merchant → write PSP config → flip KYB flag) that operators used to walk by hand.
All admin endpoints below require:
* **JWT** with `role = SUPER_ADMIN`
* **`IpAllowlistGuard`** — caller IP must be in the operator allowlist
* **`GeoBlockGuard`** — caller geo must be permitted
Calls that miss any of the three return `403`.
## Partnership-PSP preset
Each merchant payload includes a `partnership` field. The provisioner reads the partnership
registry and applies the matching preset:
| Partnership | PSPs wired | Default MoR |
| ----------------------- | ---------------------- | ----------- |
| `shopsadiq-telr-gcc` | Telr | Shopsadiq |
| `mcredit-bonum-mn` | Bonum | MCredit |
| `stripe-direct-us` | Stripe | Merchant |
| `paypal-direct-eu` | PayPal | Merchant |
| `aggregator-shopsadiq` | Stripe + PayPal + Telr | Shopsadiq |
| `aggregator-droplinked` | Stripe + PayPal | Droplinked |
If `partnership` is omitted, the provisioner falls back to the region default
(documented in the PSP × MoR cohort taxonomy).
## POST /admin/aggregate-merchant-provisioner/single
Provisions one merchant.
### Authentication
| Guard | Requirement |
| ------------ | --------------------------------- |
| JWT | Required, `role = SUPER_ADMIN` |
| IP allowlist | Caller IP in `ADMIN_IP_ALLOWLIST` |
| Geo | Country in `ADMIN_GEO_ALLOWLIST` |
### Request body
```json theme={null}
{
"email": "founder@unstoppable.example",
"shopName": "Unstoppable",
"region": "GCC",
"partnership": "shopsadiq-telr-gcc",
"kybCohort": "A-MoR-Shopsadiq-via-Telr",
"contact": {
"fullName": "Test Operator",
"phone": "+9715XXXXXXXX"
},
"options": {
"skipKybVerification": false,
"seedDemoProducts": true
}
}
```
| Field | Type | Required | Description |
| ----------------------------- | ----------------------------------------------- | -------- | ------------------------------------------------------------- |
| `email` | string | Yes | Merchant primary email; becomes the JWT `sub` for the owner |
| `shopName` | string | Yes | Storefront name; subdomain auto-generated from this |
| `region` | enum (`US` \| `EU` \| `GCC` \| `MN` \| `OTHER`) | Yes | Drives PSP defaults and FX rounding |
| `partnership` | string | No | Partnership preset key; falls back to region default |
| `kybCohort` | string | No | One of the 8 cohort enums; defaults to `A-Connect` |
| `contact.fullName` | string | Yes | Real name of the merchant contact |
| `contact.phone` | string | No | E.164 phone |
| `options.skipKybVerification` | boolean | No | Defaults `false`; only set `true` for internal test merchants |
| `options.seedDemoProducts` | boolean | No | Defaults `false`; seeds 3 demo SKUs for walkthrough demos |
### Response — 201 Created
```json theme={null}
{
"merchantId": "65f8a1b2c3d4e5f6a7b8c9aa",
"shopId": "65f8a1b2c3d4e5f6a7b8c9bb",
"subdomain": "unstoppable",
"ownerUserId": "65f8a1b2c3d4e5f6a7b8c9cc",
"partnership": "shopsadiq-telr-gcc",
"kybCohort": "A-MoR-Shopsadiq-via-Telr",
"pspsProvisioned": ["telr"],
"kybStatus": "PENDING",
"seededProducts": 3,
"invitationEmailSent": true
}
```
### Error responses
| Status | When |
| ------ | --------------------------------------------------------------------- |
| `400` | Missing/invalid field; unknown `partnership` key; unknown `kybCohort` |
| `403` | JWT / IP / geo guard failed |
| `409` | A merchant already exists at this email or subdomain |
### Example
```bash theme={null}
curl -X POST \
https://apiv3.droplinked.com/admin/aggregate-merchant-provisioner/single \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"email": "founder@unstoppable.example",
"shopName": "Unstoppable",
"region": "GCC",
"partnership": "shopsadiq-telr-gcc",
"kybCohort": "A-MoR-Shopsadiq-via-Telr",
"contact": { "fullName": "Test Operator" }
}'
```
## POST /admin/aggregate-merchant-provisioner/bulk
Provisions an array of merchants. Each entry is attempted independently — a failure on one
does **not** abort the batch. The response partitions results into `successful` and
`failed`.
### Request body
```json theme={null}
{
"merchants": [
{ "email": "a@example.com", "shopName": "A", "region": "GCC", "partnership": "shopsadiq-telr-gcc", "contact": { "fullName": "A" } },
{ "email": "b@example.com", "shopName": "B", "region": "MN", "partnership": "mcredit-bonum-mn", "contact": { "fullName": "B" } }
]
}
```
| Field | Type | Required | Description |
| ----------- | ------------- | -------- | ------------------------------------ |
| `merchants` | array (1-100) | Yes | Array of single-provisioner payloads |
### Response — 207 Multi-Status
```json theme={null}
{
"submitted": 2,
"successful": [
{
"index": 0,
"email": "a@example.com",
"merchantId": "65f8a1b2c3d4e5f6a7b8c9aa",
"subdomain": "a"
}
],
"failed": [
{
"index": 1,
"email": "b@example.com",
"error": "Subdomain 'b' already taken",
"code": "SUBDOMAIN_CONFLICT"
}
]
}
```
### Error responses
| Status | When |
| ------ | ------------------------------------------------------------------ |
| `400` | Array empty, > 100 entries, or every entry is structurally invalid |
| `403` | JWT / IP / geo guard failed |
The batch HTTP status is `207 Multi-Status` whenever at least one entry succeeds and at
least one fails; `201` when all succeed; `400` when all fail validation.
## GET /admin/aggregate-merchant-provisioner/preview
Dry-run: validates payloads and resolves partnership presets, but **does not write**.
Useful for "what would happen if I bulk-provisioned this CSV?" before committing.
Accepts the same body shape as the bulk endpoint, sent via `GET` with the payload in the
request body (Mintlify renders this — the backend reads JSON from the request body on this
route specifically).
### Response — 200 OK
```json theme={null}
{
"submitted": 2,
"wouldSucceed": [
{
"index": 0,
"email": "a@example.com",
"subdomain": "a",
"pspsThatWouldProvision": ["telr"],
"kybCohortResolved": "A-MoR-Shopsadiq-via-Telr"
}
],
"wouldFail": [
{
"index": 1,
"email": "b@example.com",
"error": "Subdomain 'b' already taken",
"code": "SUBDOMAIN_CONFLICT"
}
],
"warnings": [
"Merchant 'a@example.com' has no `kybCohort` — would default to `A-Connect` based on region GCC"
]
}
```
### Example
```bash theme={null}
curl -X GET \
https://apiv3.droplinked.com/admin/aggregate-merchant-provisioner/preview \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"merchants": [
{ "email": "a@example.com", "shopName": "A", "region": "GCC", "contact": { "fullName": "A" } }
]
}'
```
## Operational notes
* **Idempotency:** the provisioner is **not** idempotent. Re-submitting the same email
returns `409`. Use the preview endpoint to scrub a CSV before running bulk.
* **Email invitations:** the owner-user invitation email is sent **synchronously** on
successful provision. If your operator workflow sends a custom welcome email, set
`options.skipInvitationEmail` (boolean, default `false`).
* **Cohort routing:** the `kybCohort` field flows downstream into the lending eligibility,
cost-comparator, and affiliate-commission projections. Choose intentionally.
## Related
* [Bonum admin](/api-reference/admin/bonum) — per-merchant Bonum config (set after provisioning when partnership = `mcredit-bonum-mn`).
* [Telr admin](/api-reference/admin/telr) — Telr reconcile for partnerships that wire Telr.
* [Network Health KPIs](/api-reference/admin/kpi-dashboard) — track verified-shops + GMV growth after bulk provisioning.
# Bonum Admin
Source: https://docs.droplinked.com/api-reference/admin/bonum
Operator endpoints for Bonum PSP — manual webhook reconcile + per-merchant configuration CRUD.
The Bonum admin surface gives Droplinked operators two capabilities:
1. **Manual reconciliation** — one-shot recovery for orders stuck in `PENDING` because the
sandbox (or, occasionally, prod) failed to fire a settlement webhook.
2. **Per-merchant configuration** — onboard a Bonum-MoR or Bonum-Direct merchant by writing
their terminal credentials directly, without an env-flag round-trip.
All admin endpoints below require:
* **JWT** with `role = SUPER_ADMIN`
* **`IpAllowlistGuard`** — caller IP must be in the operator allowlist
* **`GeoBlockGuard`** — caller geo must be permitted (GCC/US/EU by default)
Calls that miss any of the three return `403`.
## When to use
| Situation | Endpoint |
| --------------------------------------------------------------------------------- | -------------------------------------------- |
| Order is `PENDING` in dev because Bonum sandbox did not POST `/bonum/webhook` | `POST /admin/bonum/reconcile/:transactionId` |
| Tugsjargal (or operator partner) sent new prod creds for a merchant | `PUT /admin/bonum/config/:merchantId` |
| Need to confirm the active config for a merchant before debugging a failed charge | `GET /admin/bonum/config/:merchantId` |
| Move a merchant back to env-default fallback | `DELETE /admin/bonum/config/:merchantId` |
## POST /admin/bonum/reconcile/:transactionId
Manually reconciles a single Bonum transaction. Bypasses the
`BONUM_RECONCILIATION_ENABLED` env flag — this endpoint is intended for stuck-order recovery,
so the flag check is intentionally skipped.
Accepts **either**:
* The Bonum invoice ID (24-char alphanumeric returned by `POST /orders/v2/create-payment-intent`)
* Our internal order `ObjectId` (24-char hex)
The handler resolves the supplied ID to the canonical `BonumTransaction`, calls Bonum's
`/api/payment-log/read` to refetch settlement state, then writes through to the order and
unified-transaction projection.
### Authentication
| Guard | Requirement |
| ------------ | --------------------------------- |
| JWT | Required, `role = SUPER_ADMIN` |
| IP allowlist | Caller IP in `ADMIN_IP_ALLOWLIST` |
| Geo | Country in `ADMIN_GEO_ALLOWLIST` |
### Path parameters
| Param | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------------- |
| `transactionId` | string | Yes | Bonum `invoiceId` **or** our order `ObjectId` |
### Request body
Empty — `POST` with no body.
### Response — 200 OK
```json theme={null}
{
"transactionId": "65f8a1b2c3d4e5f6a7b8c9d0",
"invoiceId": "ABC123DEF456GHI789JKL012",
"merchantId": "65f8a1b2c3d4e5f6a7b8c9aa",
"previousStatus": "PENDING",
"currentStatus": "SETTLED",
"settledAt": "2026-06-04T12:34:56.789Z",
"amount": 100000,
"currency": "MNT",
"reconciledVia": "manual-admin",
"orderUpdated": true,
"unifiedTransactionUpdated": true
}
```
### Error responses
| Status | When |
| ------ | -------------------------------------------------------------------------- |
| `400` | `transactionId` is neither a Bonum invoice ID nor a valid order `ObjectId` |
| `403` | JWT missing / wrong role / IP or geo guard failed |
| `404` | No `BonumTransaction` matches the supplied ID |
| `502` | Bonum `/api/payment-log/read` returned a non-2xx |
| `503` | Bonum API breaker is open |
### Example
```bash theme={null}
curl -X POST \
https://apiv3.droplinked.com/admin/bonum/reconcile/65f8a1b2c3d4e5f6a7b8c9d0 \
-H "Authorization: Bearer "
```
The Bonum sandbox at `testpsp.bonum.mn` does **not** fire webhooks reliably. The reconcile
endpoint is the canonical recovery path for sandbox testing — keep its URL bookmarked next
to your Bonum sandbox test script.
## PUT /admin/bonum/config/:merchantId
Upserts the per-merchant Bonum configuration. When a `BonumConfig` row exists for a
merchant, the `BonumPaymentStrategy` uses it instead of the global env defaults.
`checksumKey` is **encrypted at rest** with the platform KMS key. The plaintext value is
required on write — it cannot be recovered after storage. `mode` and `apiBaseUrl` are
validated **together**: `mode = production` requires a production-origin `apiBaseUrl`
(no `testpsp.*` host); `mode = sandbox` rejects production hosts.
### Authentication
| Guard | Requirement |
| ------------ | --------------------------------- |
| JWT | Required, `role = SUPER_ADMIN` |
| IP allowlist | Caller IP in `ADMIN_IP_ALLOWLIST` |
| Geo | Country in `ADMIN_GEO_ALLOWLIST` |
### Path parameters
| Param | Type | Required | Description |
| ------------ | ------------------- | -------- | ---------------------- |
| `merchantId` | string (`ObjectId`) | Yes | Droplinked merchant ID |
### Request body
```json theme={null}
{
"terminalId": "TERM-MN-0042",
"checksumKey": "raw-checksum-key-from-bonum",
"apiBaseUrl": "https://psp.bonum.mn",
"mode": "production"
}
```
| Field | Type | Required | Description |
| ------------- | -------------------------------- | -------- | ----------------------------------------------------------------------- |
| `terminalId` | string | Yes | Bonum terminal identifier issued by Bonum / MCredit |
| `checksumKey` | string | Yes | Plaintext checksum key — encrypted server-side before persist |
| `apiBaseUrl` | string (URL) | Yes | `https://testpsp.bonum.mn` for sandbox, `https://psp.bonum.mn` for prod |
| `mode` | enum (`sandbox` \| `production`) | Yes | Must match the host class of `apiBaseUrl` |
### Response — 200 OK
```json theme={null}
{
"merchantId": "65f8a1b2c3d4e5f6a7b8c9aa",
"terminalId": "TERM-MN-0042",
"checksumKey": "***",
"apiBaseUrl": "https://psp.bonum.mn",
"mode": "production",
"createdAt": "2026-06-04T10:00:00.000Z",
"updatedAt": "2026-06-04T10:00:00.000Z"
}
```
### Error responses
| Status | When |
| ------ | ----------------------------------------------------------------------------------------- |
| `400` | Missing field; `mode`/`apiBaseUrl` mismatch (e.g. `mode=production` + `testpsp.bonum.mn`) |
| `403` | JWT / IP / geo guard failed |
| `404` | Merchant does not exist |
### Operator playbook — "Tugsjargal sent prod creds, how do I onboard them?"
Verify the merchantId and terminalId in 1Password / Slack DM with Tugsjargal. Do **not**
accept creds via email plaintext.
```bash theme={null}
curl -X PUT \
https://apiv3.droplinked.com/admin/bonum/config/ \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"terminalId": "",
"checksumKey": "",
"apiBaseUrl": "https://psp.bonum.mn",
"mode": "production"
}'
```
`GET /admin/bonum/config/` — confirm `mode=production`, `apiBaseUrl=https://psp.bonum.mn`,
and `checksumKey=***`. No operator action elsewhere is required; the next intent created
for this merchant will route through the prod Bonum endpoint with the new terminal.
Create a 100 MNT test intent for the merchant, complete the payment via Bonum, confirm
via `GET /admin/bonum/config/` does not change, and check the order moves
to `SETTLED` within one webhook cycle.
## GET /admin/bonum/config/:merchantId
Reads the per-merchant Bonum configuration. `checksumKey` is **always** masked to the
literal string `"***"` in the response — it is never returned in plaintext.
### Authentication
| Guard | Requirement |
| ------------ | --------------------------------- |
| JWT | Required, `role = SUPER_ADMIN` |
| IP allowlist | Caller IP in `ADMIN_IP_ALLOWLIST` |
| Geo | Country in `ADMIN_GEO_ALLOWLIST` |
### Path parameters
| Param | Type | Required | Description |
| ------------ | ------------------- | -------- | ---------------------- |
| `merchantId` | string (`ObjectId`) | Yes | Droplinked merchant ID |
### Response — 200 OK
```json theme={null}
{
"merchantId": "65f8a1b2c3d4e5f6a7b8c9aa",
"terminalId": "TERM-MN-0042",
"checksumKey": "***",
"apiBaseUrl": "https://psp.bonum.mn",
"mode": "production",
"createdAt": "2026-06-04T10:00:00.000Z",
"updatedAt": "2026-06-04T10:00:00.000Z"
}
```
### Response — 404 Not Found
Returned when no `BonumConfig` row exists for the merchant. Means the merchant is on
**env defaults** (`BONUM_API_BASE_URL`, `BONUM_MERCHANT_KEY`, etc.).
### Example
```bash theme={null}
curl https://apiv3.droplinked.com/admin/bonum/config/65f8a1b2c3d4e5f6a7b8c9aa \
-H "Authorization: Bearer "
```
## DELETE /admin/bonum/config/:merchantId
Removes the per-merchant Bonum configuration. From the next intent onward, the merchant
falls back to env defaults.
### Authentication
| Guard | Requirement |
| ------------ | --------------------------------- |
| JWT | Required, `role = SUPER_ADMIN` |
| IP allowlist | Caller IP in `ADMIN_IP_ALLOWLIST` |
| Geo | Country in `ADMIN_GEO_ALLOWLIST` |
### Path parameters
| Param | Type | Required | Description |
| ------------ | ------------------- | -------- | ---------------------- |
| `merchantId` | string (`ObjectId`) | Yes | Droplinked merchant ID |
### Response — 200 OK
```json theme={null}
{
"merchantId": "65f8a1b2c3d4e5f6a7b8c9aa",
"deleted": true,
"fallback": "env"
}
```
### Error responses
| Status | When |
| ------ | -------------------------------------------------------------------------- |
| `403` | JWT / IP / geo guard failed |
| `404` | No `BonumConfig` row to delete (idempotent caller should treat as success) |
### Example
```bash theme={null}
curl -X DELETE \
https://apiv3.droplinked.com/admin/bonum/config/65f8a1b2c3d4e5f6a7b8c9aa \
-H "Authorization: Bearer "
```
## Related
* [Bonum integration guide](/guides/integrations/bonum) — payment flow, prefix registry, sandbox test plan.
* [Telr admin endpoints](/api-reference/admin/telr) — same reconcile pattern for the GCC PSP.
* [Aggregate merchant provisioner](/api-reference/admin/aggregate-provisioner) — bulk provisioning that may include Bonum config.
# Network Health KPIs
Source: https://docs.droplinked.com/api-reference/admin/kpi-dashboard
SuperAdmin dashboard endpoints — registered users, verified shops, confirmed orders, GMV, refund rate, top merchants/products, operations summary.
The Network Health KPI endpoints power the SuperAdmin dashboard. Every endpoint returns
aggregated, network-wide metrics — no per-tenant scoping. All require operator-level access.
All admin endpoints below require:
* **JWT** with `role = SUPER_ADMIN`
* **`IpAllowlistGuard`** — caller IP must be in the operator allowlist
* **`GeoBlockGuard`** — caller geo must be permitted
Calls that miss any of the three return `403`.
## Common query parameters (KPI endpoints)
| Param | Type | Required | Description |
| ----------- | ---------------------------- | -------- | --------------------- |
| `startDate` | ISO-8601 date (`YYYY-MM-DD`) | Yes | Inclusive lower bound |
| `endDate` | ISO-8601 date (`YYYY-MM-DD`) | Yes | Inclusive upper bound |
Date range is capped to 365 days. Requests outside the cap return `400`.
## GET /admin/dashboard/registered-users
Total new platform-user signups (merchant + customer) within the window.
### Response — 200 OK
```json theme={null}
{
"startDate": "2026-05-01",
"endDate": "2026-05-31",
"totalRegistered": 1247,
"breakdown": {
"merchant": 89,
"customer": 1158
},
"previousPeriodTotal": 982,
"percentChange": 26.99
}
```
### Example
```bash theme={null}
curl "https://apiv3.droplinked.com/admin/dashboard/registered-users?startDate=2026-05-01&endDate=2026-05-31" \
-H "Authorization: Bearer "
```
## GET /admin/dashboard/verified-shops
Shops that completed KYB **within the window** (i.e., became `KYB_VERIFIED`).
### Response — 200 OK
```json theme={null}
{
"startDate": "2026-05-01",
"endDate": "2026-05-31",
"verifiedShops": 42,
"breakdown": {
"tier1Lending": 12,
"tier2Standard": 30
},
"previousPeriodTotal": 31,
"percentChange": 35.48
}
```
## GET /admin/dashboard/confirmed-orders
Orders that transitioned to `CONFIRMED` (payment captured + saga complete) within the window.
### Response — 200 OK
```json theme={null}
{
"startDate": "2026-05-01",
"endDate": "2026-05-31",
"confirmedOrders": 8842,
"breakdown": {
"stripe": 4221,
"paypal": 2410,
"bonum": 612,
"telr": 989,
"paymob": 401,
"coinbase": 209
},
"previousPeriodTotal": 7123,
"percentChange": 24.13
}
```
## GET /admin/dashboard/created-products
Products created (any merchant) within the window.
### Response — 200 OK
```json theme={null}
{
"startDate": "2026-05-01",
"endDate": "2026-05-31",
"createdProducts": 3210,
"previousPeriodTotal": 2890,
"percentChange": 11.07
}
```
## GET /admin/dashboard/gmv
Network GMV (gross merchandise value) for confirmed orders, expressed in **USD-equivalent**
using the same FX snapshot table that the unified-transaction projection uses.
### Response — 200 OK
```json theme={null}
{
"startDate": "2026-05-01",
"endDate": "2026-05-31",
"gmvUsd": 1842310.55,
"currency": "USD",
"breakdown": {
"stripe": 921105.10,
"paypal": 412882.40,
"bonum": 152098.02,
"telr": 244112.99,
"paymob": 81221.77,
"coinbase": 30890.27
},
"previousPeriodGmvUsd": 1493022.19,
"percentChange": 23.40
}
```
## GET /admin/dashboard/refund-rate
Refund rate = `refundedAmount / confirmedAmount` for the window, computed PSP-by-PSP and
network-wide.
### Response — 200 OK
```json theme={null}
{
"startDate": "2026-05-01",
"endDate": "2026-05-31",
"networkRefundRate": 0.0231,
"breakdown": {
"stripe": 0.0188,
"paypal": 0.0312,
"bonum": 0.0049,
"telr": 0.0181,
"paymob": 0.0240,
"coinbase": 0.0102
},
"previousPeriodRefundRate": 0.0289,
"deltaBps": -58
}
```
## GET /admin/affiliate/top-merchants
Top merchants by affiliate-driven GMV, limited to `limit` (default 10, max 100).
### Query parameters
| Param | Type | Required | Description |
| ------- | --------------- | -------- | ------------------------ |
| `limit` | integer (1-100) | No | Max rows; defaults to 10 |
### Response — 200 OK
```json theme={null}
{
"limit": 10,
"results": [
{
"merchantId": "65f8a1b2c3d4e5f6a7b8c9aa",
"shopName": "Unstoppable",
"affiliateGmvUsd": 41209.55,
"affiliateOrderCount": 312
}
]
}
```
### Example
```bash theme={null}
curl "https://apiv3.droplinked.com/admin/affiliate/top-merchants?limit=25" \
-H "Authorization: Bearer "
```
## GET /admin/affiliate/top-products
Top products by affiliate-driven units sold.
### Query parameters
| Param | Type | Required | Description |
| ------- | --------------- | -------- | ------------------------ |
| `limit` | integer (1-100) | No | Max rows; defaults to 10 |
### Response — 200 OK
```json theme={null}
{
"limit": 10,
"results": [
{
"productId": "65f8a1b2c3d4e5f6a7b8c9bb",
"title": "Limited-edition hoodie",
"merchantId": "65f8a1b2c3d4e5f6a7b8c9aa",
"shopName": "Unstoppable",
"affiliateUnitsSold": 412,
"affiliateGmvUsd": 12399.40
}
]
}
```
## GET /admin/operations/services
Live operational status of the ECS services that make up the platform — fetched from the
ECS describe-services API and cached for 60s.
### Response — 200 OK
```json theme={null}
{
"fetchedAt": "2026-06-04T12:00:00.000Z",
"services": [
{
"name": "apiv3",
"cluster": "droplinked-prod",
"desiredCount": 4,
"runningCount": 4,
"rolloutState": "COMPLETED",
"taskDefinition": "apiv3:609",
"lastDeploymentAt": "2026-06-04T08:14:22.000Z"
},
{
"name": "3rdp",
"cluster": "droplinked-prod",
"desiredCount": 2,
"runningCount": 2,
"rolloutState": "COMPLETED",
"taskDefinition": "3rdp:351",
"lastDeploymentAt": "2026-06-04T09:02:11.000Z"
}
]
}
```
### When to use
The first thing to hit when an operator says "is the platform up?" — confirms desired vs
running task count and current task-definition revision per service.
## GET /admin/operations/sentry-summary
Rolling 24-hour Sentry summary, scoped to the org's main projects (apiv3, 3rdp, checkout,
shop-builder).
### Response — 200 OK
```json theme={null}
{
"windowHours": 24,
"fetchedAt": "2026-06-04T12:00:00.000Z",
"projects": [
{
"slug": "apiv3",
"newIssues": 2,
"totalEvents": 41,
"trend": "down",
"topIssue": {
"shortId": "APIV3-9C3",
"title": "TypeError: Cannot read property 'unifiedRef' of undefined",
"count": 12,
"lastSeen": "2026-06-04T11:54:00.000Z"
}
}
]
}
```
## GET /admin/operations/deploys/latest
The most recent successful deploy per service, joined with the GitHub PR / commit that
shipped it. Useful when triaging a regression — "what changed in the last 6 hours?"
### Response — 200 OK
```json theme={null}
{
"fetchedAt": "2026-06-04T12:00:00.000Z",
"deploys": [
{
"service": "apiv3",
"taskDefinition": "apiv3:609",
"deployedAt": "2026-06-04T08:14:22.000Z",
"commit": "abc1234",
"branch": "main",
"actor": "github-actions[bot]",
"pr": {
"number": 1576,
"title": "feat(checkout): public payment-intent resolver"
}
}
]
}
```
## Related
* [PSP health](/api-reference/admin/psp-health) — per-PSP breaker + probe state.
* [Aggregate merchant provisioner](/api-reference/admin/aggregate-provisioner) — bulk merchant onboarding.
* [Bonum admin](/api-reference/admin/bonum) — Bonum-specific reconcile + config.
* [Telr admin](/api-reference/admin/telr) — Telr-specific reconcile.
# Platform fees revenue rollup (admin)
Source: https://docs.droplinked.com/api-reference/admin/monetization-platform-fees
Per-PSP / per-merchant / per-day rollup of platform fees + droplinked revenue from the canonical unified_transactions collection. SUPER_ADMIN only.
`GET /admin/monetization/platform-fees` returns a **revenue rollup** of every droplinked-monetized transaction in the requested window, aggregated by PSP / merchant / day. Operators use it to answer: "how much is droplinked actually earning, and from which surfaces?"
P1 of the Monetization Pillar. This endpoint is the operator's primary visibility into droplinked revenue across the trust + commerce stack. The merchant-facing billing history endpoint (P2) ships separately so the read paths stay cleanly scoped per audience.
## When to use this
* The operator wants a real-time snapshot of droplinked revenue across all PSPs
* A finance review needs the gross / fees / net split per merchant or per PSP
* An ops investigation needs to confirm the fee\_breakdown projector is healthy (`degraded: false`)
## Authentication
`JwtAuthGuard` + `SuperAdminGuard`. A non-admin JWT returns `403`.
## Request
```
GET /admin/monetization/platform-fees?from=ISO&to=ISO&groupBy=psp|merchant|day
```
| Param | Type | In | Notes |
| --------- | --------------- | --------------- | --------------------------------------------------------------- |
| `from` | ISO-8601 string | query, optional | Window start (inclusive). Default: 30 days ago (UTC now − 30d). |
| `to` | ISO-8601 string | query, optional | Window end (exclusive). Default: UTC now. |
| `groupBy` | enum | query, optional | `psp` (default) \| `merchant` \| `day`. |
### Curl example
```bash theme={null}
curl -s "https://apiv3.droplinked.com/admin/monetization/platform-fees?from=2026-06-01&to=2026-06-30&groupBy=psp" \
-H "Authorization: Bearer $ADMIN_JWT" | jq .
```
## Response (200)
```json theme={null}
{
"windowStart": "2026-06-01T00:00:00.000Z",
"windowEnd": "2026-06-30T00:00:00.000Z",
"groupBy": "psp",
"totals": {
"grossUsd": 12480.50,
"feesUsd": 624.03,
"droplinkedRevenueUsd": 124.81,
"merchantNetUsd": 11856.47,
"txCount": 312
},
"rows": [
{
"key": "stripe",
"label": "Stripe",
"grossUsd": 9800.00,
"feesUsd": 490.00,
"droplinkedRevenueUsd": 98.00,
"merchantNetUsd": 9310.00,
"txCount": 244
},
{
"key": "paypal",
"label": "Paypal",
"grossUsd": 2680.50,
"feesUsd": 134.03,
"droplinkedRevenueUsd": 26.81,
"merchantNetUsd": 2546.47,
"txCount": 68
}
],
"partialErrors": [],
"degraded": false
}
```
### Field reference
| Field | Type | Notes |
| ----------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `windowStart` / `windowEnd` | ISO-8601 string | Effective window (defaults filled in if not supplied). |
| `groupBy` | enum | Echoes the request's groupBy. |
| `totals.grossUsd` | number | Sum of `amount_usd_equivalent` across rows in the window. |
| `totals.feesUsd` | number | Sum of `amount_fees_usd` (PSP + MoR + droplinked combined). |
| `totals.droplinkedRevenueUsd` | number | Sum of `feeBreakdown.droplinked_gmv_fee_usd + droplinked_funding_fee_usd + droplinked_saas_fee_usd`. **This is droplinked's actual revenue slice** — distinct from the wider fees pool. |
| `totals.merchantNetUsd` | number | Sum of `amount_net_usd` (net to merchant after all fees). |
| `totals.txCount` | int | Completed-transaction count in the window. |
| `rows[]` | array | Per-slice rollup. Sorted by `droplinkedRevenueUsd` desc except when `groupBy=day` (chronological). |
| `rows[].key` | string | Group key — PSP name / merchant\_id hex / `YYYY-MM-DD`. |
| `rows[].label` | string | Display label for the row. |
| `partialErrors[]` | array | Rows that could not be processed (e.g. malformed `fee_breakdown`). Each entry: `{ slice: string, error: string }`. |
| `degraded` | boolean | `true` iff `partialErrors.length > 0`. UI surfaces show a "partial data" pill when set. |
## Money fields are decimal USD (NOT cents)
Unlike the abandoned-cart list endpoint (which serializes integer minor units), this endpoint returns all USD values as **decimals**. This matches the canonical `unified_transactions` collection's persisted shape — the projector's contract (`ProjectionInputDto`) is explicit: "decimal, NOT minor units (canonical schema convention)."
Clients pass values straight into `Intl.NumberFormat({ style: 'currency', currency: 'USD' })` without dividing by 100.
## Fail-open posture
A row with a malformed `fee_breakdown` is **NOT** a 500. The service emits a `partialErrors` entry naming the offending row's PSP transaction id; the response still ships the rollup across the rows that succeeded. `degraded: true` lets the UI render a visible "showing partial data" badge.
This matches the broader [Stripe Head of Platform fail-open backbone discipline](/guides/security/supply-chain) — never crash a read path on a single bad row. UI consumers should always check `degraded` and surface it to the operator.
## Group-by behaviors
| `groupBy` | What rows look like |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `psp` | One row per PSP (`stripe`, `paypal`, `telr`, `bonum`, `paymob`). Most-monetizing PSP first. Excludes Coinbase (sunset). |
| `merchant` | One row per `merchant_id` (hex). Most-monetizing merchant first. Use the SUPER\_ADMIN merchant directory to resolve hex IDs to display names. |
| `day` | One row per `YYYY-MM-DD` (UTC) in the window. Chronological order — read as a time series. |
## Error responses
| Status | When |
| ------ | --------------------------------------- |
| `400` | Malformed `from` / `to` (not ISO-8601). |
| `401` | Missing / invalid JWT. |
| `403` | Caller is not a SUPER\_ADMIN. |
## Architecture notes
* Reads canonical `unified_transactions` collection via Mongoose connection — no new schema introduced (every field already projected at PSP-webhook landing time).
* Sums use **banker's rounding to 2 decimals** (Stripe Billing Head of Product discipline).
* `completed` status only — `pending` / `failed` / `chargeback` / `refunded` rows excluded. Refund + chargeback rollups will surface as separate slices in P2 follow-ups (Stripe Billing convention: refund / chargeback paths are first-class, never an exception path).
* Coinbase is excluded from the providers enum per the [Coinbase sunset](https://github.com/droplinked/.github/blob/main/CODEOWNERS) memo.
## Related
* [Abandoned cart recovery (public)](/api-reference/public/abandoned-cart-recovery) — sister fees mechanic on the recovery side
* [Order lifecycle](/guides/order-lifecycle) — chokepoint that flips a row to `completed`
* [KPI dashboard (admin)](/api-reference/admin/kpi-dashboard) — sibling operator-visibility surface
* [PSP health (admin)](/api-reference/admin/psp-health) — sibling per-PSP operator surface
# Capital Markets Fees
Source: https://docs.droplinked.com/api-reference/admin/monetization/capital-markets-fees
Admin fee-schedule preview for the Capital Markets marketplace. Shipping in a follow-up — endpoint not yet exposed on apiv3 prod.
`GET /admin/monetization/capital-markets-fees` will return the operator-facing
fee-schedule preview for the **Capital Markets marketplace** — the planned
marketplace where droplinked attests merchant credit signals, lenders compete on
rate, and financiers gain tier-bucketed exposure.
**This endpoint is not yet live on `apiv3.droplinked.com` prod (verified 2026-06-14
— request returns `404`).** The route is being staged behind the Pillar 5 P4
workstream. This page is a placeholder reserved in the navigation so the URL stays
stable once the endpoint deploys. Re-probe and fill in the live response shape
before relying on it.
Tracking: Capital Markets Marketplace playbook (2026-06-13) — Pillar 5 P4.
## GET /admin/monetization/capital-markets-fees
### Authentication
| Guard | Requirement |
| ------------ | --------------------------------- |
| JWT | Required, `role = SUPER_ADMIN` |
| IP allowlist | Caller IP in `ADMIN_IP_ALLOWLIST` |
| Geo | Country in `ADMIN_GEO_ALLOWLIST` |
Same guard stack as every other admin endpoint — see
[Network Health KPIs](/api-reference/admin/kpi-dashboard) and
[Authentication](/authentication).
### Current behaviour on prod
```bash theme={null}
curl "https://apiv3.droplinked.com/admin/monetization/capital-markets-fees" \
-H "Authorization: Bearer "
```
```json theme={null}
{
"statusCode": 404,
"status": "failed",
"data": {
"message": "Cannot GET /admin/monetization/capital-markets-fees"
}
}
```
### Planned scope
When the endpoint ships it will surface:
* Marketplace tier brackets (AAA / AA / A / BBB / BB / B / CCC) and their
associated fee schedule (origination bps, servicing bps, droplinked attestor bps).
* Cohort grid pointer (the snapshot powering tier assignment).
* Effective-as-of timestamp for the schedule.
The shape is intentionally not documented here yet — per the docs-discipline rule,
fields must be live-probed on prod before being documented. Once the route deploys,
update this page from the live response.
## Related
* [Platform Fee Summary](/api-reference/admin/monetization/platform-fee-summary) — MRR / ARR / 30d / 365d rollup; will include the capital-markets fee constituent once the marketplace settles its first cohort.
* [x402 Earnings](/api-reference/admin/monetization/x402-earnings) — the other Pillar 5 read-only earnings rollup.
* [Set Merchant Acquisition Source](/api-reference/admin/monetization/merchant-acquisition-source) — operator attribution setter that feeds the activation-method mix.
# Set Merchant Acquisition Source
Source: https://docs.droplinked.com/api-reference/admin/monetization/merchant-acquisition-source
Operator-only setter for a merchant's AcquisitionSource enum. Drives the activation-method mix in the platform-fee summary and feeds attribution rollups.
`PATCH /admin/merchants/:id/acquisition-source` sets (or clears) the
`acquisitionSource` field on a `MerchantV2` record. The value drives the
`activationMethodMix` rollup surfaced by
[`GET /admin/monetization/platform-fee-summary`](/api-reference/admin/monetization/platform-fee-summary)
and feeds downstream attribution (Impact, Awin, referral payouts, agentic-affiliate
intake).
This endpoint requires:
* **JWT** with `role = SUPER_ADMIN`
* **`IpAllowlistGuard`** — caller IP must be in the operator allowlist
* **`GeoBlockGuard`** — caller geo must be permitted
Calls that miss any of the three return `403`. Missing or invalid JWT returns `401`.
## PATCH /admin/merchants/:id/acquisition-source
### Authentication
| Guard | Requirement |
| ------------ | --------------------------------- |
| JWT | Required, `role = SUPER_ADMIN` |
| IP allowlist | Caller IP in `ADMIN_IP_ALLOWLIST` |
| Geo | Country in `ADMIN_GEO_ALLOWLIST` |
Obtain a SUPER\_ADMIN JWT via `POST /merchant/admin/login` — see
[Authentication](/authentication).
### Path parameters
| Param | Type | Required | Description |
| ----- | ----------------- | -------- | ---------------------------------------- |
| `id` | string (ObjectId) | Yes | `MerchantV2._id` of the target merchant. |
### Request body
| Field | Type | Required | Description |
| -------- | ----------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `source` | enum string | No | One of `IMPACT`, `AWIN`, `REFERRAL`, `AGENTIC_AFFILIATE_INTAKE`, `OTHER`, `UNKNOWN`. Omit or send `{}` to clear the field (sets `acquisitionSource` to `null`). |
### Example — set
```bash theme={null}
curl -X PATCH "https://apiv3.droplinked.com/admin/merchants/6794172fefa4eb734620b00c/acquisition-source" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{"source":"IMPACT"}'
```
### Example — clear
```bash theme={null}
curl -X PATCH "https://apiv3.droplinked.com/admin/merchants/6794172fefa4eb734620b00c/acquisition-source" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{}'
```
### Response — 200 OK
```json theme={null}
{
"statusCode": 200,
"message": null,
"data": {
"merchantId": "6794172fefa4eb734620b00c",
"acquisitionSource": "IMPACT"
}
}
```
When cleared:
```json theme={null}
{
"statusCode": 200,
"message": null,
"data": {
"merchantId": "6794172fefa4eb734620b00c",
"acquisitionSource": null
}
}
```
### Fields
| Field | Type | Nullable | Description |
| ------------------------ | ----------------- | -------- | ------------------------------------------- |
| `statusCode` | integer | No | Always `200` on success. |
| `message` | string | Yes | Operator-facing message; `null` on success. |
| `data.merchantId` | string (ObjectId) | No | Echoed `:id` path param. |
| `data.acquisitionSource` | enum string | Yes | The new value, or `null` if cleared. |
### Errors
| Status | Body | When |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- |
| `400` | `{ "statusCode": 400, "status": "failed", "data": { "message": "source must be one of the following values: IMPACT, AWIN, REFERRAL, AGENTIC_AFFILIATE_INTAKE, OTHER, UNKNOWN, " } }` | `source` is present but not in the enum |
| `401` | `{ "statusCode": 401, "status": "failed", "data": { "message": "Unauthorized" } }` | Missing or invalid JWT |
| `403` | `{ "statusCode": 403, "status": "failed", "data": { "message": "Forbidden" } }` | JWT valid but not SUPER\_ADMIN, or IP / geo guard failed |
| `500` | `{ "statusCode": 500, "status": "failed", "data": { "message": "...No record was found for an update..." } }` | `:id` does not match any `MerchantV2` record |
### Notes
* **Operator-only.** This is an operator-driven attribution backfill — no merchant or
partner surface invokes it. The setter is intentionally stateless beyond the field
write (no audit log entry surfaced in the response; the underlying admin actor
audit captures the call).
* **Enum semantics.**
* `IMPACT` — sourced via the Impact.com partner network (Flatlay account).
* `AWIN` — sourced via the Awin partner network.
* `REFERRAL` — operator-driven referral, no partner network.
* `AGENTIC_AFFILIATE_INTAKE` — sourced via the agentic-affiliate intake flow (chat / MCP).
* `OTHER` — sourced through a named channel that doesn't map to the above.
* `UNKNOWN` — sourced through an unknown channel (default for backfilled legacy merchants).
* **Clearing.** Omitting `source` (or sending `{}` / `null`) clears the field rather
than rejecting the request. Use clearing to roll back an incorrect attribution.
* **Effect on rollups.** The next
[`GET /admin/monetization/platform-fee-summary`](/api-reference/admin/monetization/platform-fee-summary)
call recomputes `activationMethodMix` from the live store, so changes appear
immediately on the next read.
## Related
* [Platform Fee Summary](/api-reference/admin/monetization/platform-fee-summary) — `activationMethodMix` is keyed off this field.
* [x402 Earnings](/api-reference/admin/monetization/x402-earnings) — per-merchant x402 rollup.
* [Aggregate Provisioner](/api-reference/admin/aggregate-provisioner) — bulk merchant onboarding (sets `acquisitionSource` at create time).
# Platform Fee Summary
Source: https://docs.droplinked.com/api-reference/admin/monetization/platform-fee-summary
Admin read-only revenue rollup — MRR / ARR / trailing 30d / trailing 365d / activation-method mix. ADMIN comps excluded from $ totals, surfaced in the mix array. Fail-open semantics.
`GET /admin/monetization/platform-fee-summary` returns a network-wide rollup of
platform-fee revenue (subscription + x402 + capital-markets, where enabled) plus a
shop-activation method breakdown. The endpoint is the data source for the operator
**Pillar 5 revenue dashboard**.
This endpoint requires:
* **JWT** with `role = SUPER_ADMIN`
* **`IpAllowlistGuard`** — caller IP must be in the operator allowlist
* **`GeoBlockGuard`** — caller geo must be permitted
Calls that miss any of the three return `403`. Missing or invalid JWT returns `401`.
## GET /admin/monetization/platform-fee-summary
### Authentication
| Guard | Requirement |
| ------------ | --------------------------------- |
| JWT | Required, `role = SUPER_ADMIN` |
| IP allowlist | Caller IP in `ADMIN_IP_ALLOWLIST` |
| Geo | Country in `ADMIN_GEO_ALLOWLIST` |
Obtain a SUPER\_ADMIN JWT via `POST /merchant/admin/login` — see
[Authentication](/authentication).
### Query parameters
None. The window is fixed (trailing 30 / 365 days, snapshot at `asOf`).
### Example
```bash theme={null}
curl "https://apiv3.droplinked.com/admin/monetization/platform-fee-summary" \
-H "Authorization: Bearer "
```
### Response — 200 OK
```json theme={null}
{
"mrrUsdCents": 0,
"arrUsdCents": 0,
"revenue30dUsdCents": 0,
"revenue365dUsdCents": 0,
"activationMethodMix": [
{ "method": "CARD", "count": 32 },
{ "method": "ADMIN", "count": 1 }
],
"asOf": "2026-06-14T22:48:37.389Z"
}
```
### Fields
| Field | Type | Nullable | Description |
| ------------------------------ | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
| `mrrUsdCents` | integer | No | Monthly recurring revenue (active subscriptions, normalized to monthly), USD cents. ADMIN-activated shops excluded. |
| `arrUsdCents` | integer | No | Annualized recurring revenue = `mrrUsdCents * 12`. ADMIN excluded. |
| `revenue30dUsdCents` | integer | No | Settled platform-fee revenue in trailing 30 days, USD cents. ADMIN excluded. |
| `revenue365dUsdCents` | integer | No | Settled platform-fee revenue in trailing 365 days, USD cents. ADMIN excluded. |
| `activationMethodMix` | array | No | Activation method breakdown for shops in the trailing 365d window. Empty array if no activations. |
| `activationMethodMix[].method` | string | No | One of `CARD`, `ADMIN`, `REFERRAL`, `IMPACT`, `AWIN`, `AGENTIC_AFFILIATE_INTAKE`, `OTHER`, `UNKNOWN`. |
| `activationMethodMix[].count` | integer | No | Shop count for the method. |
| `asOf` | ISO-8601 string | No | Snapshot timestamp (server clock, UTC). |
### Errors
| Status | Body | When |
| ------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `401` | `{ "statusCode": 401, "status": "failed", "data": { "message": "Unauthorized" } }` | Missing or invalid JWT |
| `403` | `{ "statusCode": 403, "status": "failed", "data": { "message": "Forbidden" } }` | JWT valid but not SUPER\_ADMIN, or IP / geo guard failed |
| `5xx` | `{ "statusCode": 500, "status": "failed", ... }` | Hard backend failure — see Notes below for the fail-open contract |
### Notes
* **ADMIN-comp policy.** Shops activated via the SUPER\_ADMIN console (method = `ADMIN`)
are comp / waived and never contribute to `mrrUsdCents`, `arrUsdCents`,
`revenue30dUsdCents`, or `revenue365dUsdCents`. They DO appear in
`activationMethodMix` so operators can see the comp count.
* **Fail-open semantics.** If a constituent rollup query fails (subscription store,
x402 settlement log, capital-markets fee aggregator), the endpoint returns `0` for
that constituent (and `[]` for `activationMethodMix` if the activation source
rollup fails) rather than propagating the error. The endpoint still returns
`200 OK` with the partial snapshot. Inspect server logs / Sentry for the
underlying failure.
* **Snapshot, not realtime.** `asOf` reflects the server clock when the rollup ran.
Constituent stores are read sequentially; values are coherent within a single
request but may drift between consecutive calls.
* **Currency.** All monetary fields are USD cents, integer. FX-conversion uses the
same snapshot table as the unified-transaction projection (see
[Network Health KPIs — GMV](/api-reference/admin/kpi-dashboard#get-admin-dashboard-gmv)).
## Related
* [x402 Earnings](/api-reference/admin/monetization/x402-earnings) — per-merchant x402 settlement rollup.
* [Capital Markets Fees](/api-reference/admin/monetization/capital-markets-fees) — Capital Markets marketplace fee schedule preview.
* [Set Merchant Acquisition Source](/api-reference/admin/monetization/merchant-acquisition-source) — operator-only setter for the activation-source enum surfaced in `activationMethodMix`.
* [Network Health KPIs](/api-reference/admin/kpi-dashboard) — GMV / orders / refund-rate companions.
# x402 Earnings
Source: https://docs.droplinked.com/api-reference/admin/monetization/x402-earnings
Per-merchant x402 settlement rollup. Reads X402SettlementLog. Returns empty rows until X402_ENABLED is flipped on the platform.
`GET /admin/monetization/x402-earnings` returns the per-merchant rollup of x402
settlement events captured by the platform — gross settled amount, settlement count,
and pagination. The endpoint reads `X402SettlementLog`.
This endpoint requires:
* **JWT** with `role = SUPER_ADMIN`
* **`IpAllowlistGuard`** — caller IP must be in the operator allowlist
* **`GeoBlockGuard`** — caller geo must be permitted
Calls that miss any of the three return `403`. Missing or invalid JWT returns `401`.
## GET /admin/monetization/x402-earnings
### Authentication
| Guard | Requirement |
| ------------ | --------------------------------- |
| JWT | Required, `role = SUPER_ADMIN` |
| IP allowlist | Caller IP in `ADMIN_IP_ALLOWLIST` |
| Geo | Country in `ADMIN_GEO_ALLOWLIST` |
Obtain a SUPER\_ADMIN JWT via `POST /merchant/admin/login` — see
[Authentication](/authentication).
### Query parameters
| Param | Type | Required | Default | Description |
| ------- | ------- | -------- | ------- | --------------------- |
| `page` | integer | No | `1` | 1-indexed page number |
| `limit` | integer | No | `30` | Rows per page |
### Example
```bash theme={null}
curl "https://apiv3.droplinked.com/admin/monetization/x402-earnings?page=1&limit=30" \
-H "Authorization: Bearer "
```
### Response — 200 OK
```json theme={null}
{
"rows": [],
"totalAmountUsdCents": 0,
"totalSettlements": 0,
"pagination": {
"page": 1,
"limit": 30,
"total": 0
},
"asOf": "2026-06-14T22:58:16.027Z"
}
```
### Fields
| Field | Type | Nullable | Description |
| ---------------------------- | ----------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `rows` | array | No | One entry per merchant with at least one x402 settlement in the window. Empty array until `X402_ENABLED` is flipped on. |
| `rows[].merchantId` | string (ObjectId) | No | MerchantV2 `_id`. |
| `rows[].shopName` | string | No | Merchant's primary shop name. |
| `rows[].settlementCount` | integer | No | Number of `X402SettlementLog` entries for the merchant. |
| `rows[].grossAmountUsdCents` | integer | No | Sum of settlement amounts for the merchant, USD cents. |
| `rows[].lastSettledAt` | ISO-8601 string | Yes | Timestamp of the most recent settlement; `null` if the merchant has none. |
| `totalAmountUsdCents` | integer | No | Network-wide gross settled amount across all rows, USD cents. |
| `totalSettlements` | integer | No | Network-wide settlement count. |
| `pagination` | object | No | Pagination envelope. |
| `pagination.page` | integer | No | Echoed `page` query param (1-indexed). |
| `pagination.limit` | integer | No | Echoed `limit` query param. |
| `pagination.total` | integer | No | Total number of merchant rows matching the rollup (across pages). |
| `asOf` | ISO-8601 string | No | Snapshot timestamp (server clock, UTC). |
### Errors
| Status | Body | When |
| ------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `401` | `{ "statusCode": 401, "status": "failed", "data": { "message": "Unauthorized" } }` | Missing or invalid JWT |
| `403` | `{ "statusCode": 403, "status": "failed", "data": { "message": "Forbidden" } }` | JWT valid but not SUPER\_ADMIN, or IP / geo guard failed |
| `5xx` | `{ "statusCode": 500, "status": "failed", ... }` | Hard backend failure — see Notes below for the fail-open contract |
### Notes
* **`X402_ENABLED` gating.** Until the platform flag `X402_ENABLED` is set to `true`,
`X402SettlementLog` accepts no writes and this endpoint always returns
`rows: []`, `totalAmountUsdCents: 0`, `totalSettlements: 0`. The endpoint
itself is always reachable — the gate is at the write path, not the read path.
* **Fail-open semantics.** If the underlying `X402SettlementLog` aggregation throws,
the endpoint returns the empty-state envelope (`rows: []`, totals `0`) with
`200 OK` rather than propagating the error. Inspect server logs / Sentry for the
underlying failure.
* **Currency.** All monetary fields are USD cents, integer. x402 settles natively in
USDC on Base; the read path converts at the settlement timestamp's FX snapshot.
* **Pagination.** `total` is the number of merchant rows, not the number of
settlements. A merchant with 50 settlements counts as `1` for pagination.
## Related
* [Platform Fee Summary](/api-reference/admin/monetization/platform-fee-summary) — MRR / ARR / 30d / 365d rollup that includes x402 in `revenue30dUsdCents` / `revenue365dUsdCents`.
* [Capital Markets Fees](/api-reference/admin/monetization/capital-markets-fees) — companion fee-schedule preview.
* [Set Merchant Acquisition Source](/api-reference/admin/monetization/merchant-acquisition-source) — set the activation-source enum surfaced in the platform-fee-summary mix.
# PSP Health
Source: https://docs.droplinked.com/api-reference/admin/psp-health
Per-PSP probe + circuit-breaker status across Stripe, PayPal, Bonum, Telr, Paymob, and Coinbase.
`GET /admin/psp/health` returns the active probe state, circuit-breaker status, and
probe-enabled flag for every PSP the platform currently supports.
Probes are **OFF by default per PSP** (each has its own env flag, e.g.
`STRIPE_PROBE_ENABLED`, `BONUM_PROBE_ENABLED`). When a probe is OFF, the response still
returns the breaker state (which is always tracked) and `probeEnabled: false` so the caller
knows the probe metrics are stale.
This endpoint requires:
* **JWT** with `role = SUPER_ADMIN`
* **`IpAllowlistGuard`** — caller IP must be in the operator allowlist
* **`GeoBlockGuard`** — caller geo must be permitted
Calls that miss any of the three return `403`.
## GET /admin/psp/health
### Authentication
| Guard | Requirement |
| ------------ | --------------------------------- |
| JWT | Required, `role = SUPER_ADMIN` |
| IP allowlist | Caller IP in `ADMIN_IP_ALLOWLIST` |
| Geo | Country in `ADMIN_GEO_ALLOWLIST` |
### Query parameters
None.
### Response — 200 OK
```json theme={null}
{
"fetchedAt": "2026-06-04T12:00:00.000Z",
"psps": {
"stripe": {
"probeEnabled": true,
"probes": {
"lastRunAt": "2026-06-04T11:59:30.000Z",
"lastLatencyMs": 142,
"lastStatus": "ok",
"consecutiveFailures": 0,
"successRate24h": 0.999
},
"breaker": {
"state": "closed",
"tripsInWindow": 0,
"lastTripAt": null,
"lastResetAt": "2026-06-01T03:00:00.000Z"
}
},
"paypal": {
"probeEnabled": false,
"probes": null,
"breaker": {
"state": "closed",
"tripsInWindow": 0,
"lastTripAt": null,
"lastResetAt": null
}
},
"bonum": {
"probeEnabled": true,
"probes": {
"lastRunAt": "2026-06-04T11:59:30.000Z",
"lastLatencyMs": 318,
"lastStatus": "ok",
"consecutiveFailures": 0,
"successRate24h": 0.997
},
"breaker": {
"state": "closed",
"tripsInWindow": 0,
"lastTripAt": null,
"lastResetAt": null
}
},
"telr": {
"probeEnabled": true,
"probes": {
"lastRunAt": "2026-06-04T11:59:30.000Z",
"lastLatencyMs": 221,
"lastStatus": "ok",
"consecutiveFailures": 0,
"successRate24h": 0.998
},
"breaker": {
"state": "closed",
"tripsInWindow": 0,
"lastTripAt": null,
"lastResetAt": null
}
},
"paymob": {
"probeEnabled": false,
"probes": null,
"breaker": {
"state": "closed",
"tripsInWindow": 0,
"lastTripAt": null,
"lastResetAt": null
}
},
"coinbase": {
"probeEnabled": false,
"probes": null,
"breaker": {
"state": "closed",
"tripsInWindow": 0,
"lastTripAt": null,
"lastResetAt": null
}
}
}
}
```
### Field reference
| Field | Type | Description |
| ---------------------------------------- | ---------------------------------------- | ------------------------------------------------------------ |
| `psps..probeEnabled` | boolean | Whether the periodic synthetic probe is running for this PSP |
| `psps..probes.lastRunAt` | ISO-8601 | Last probe execution (null when disabled) |
| `psps..probes.lastLatencyMs` | integer | Round-trip latency of the most recent probe |
| `psps..probes.lastStatus` | enum (`ok` \| `degraded` \| `failing`) | Probe verdict |
| `psps..probes.consecutiveFailures` | integer | Failed probes in a row (resets on success) |
| `psps..probes.successRate24h` | float (0-1) | Rolling 24h success rate |
| `psps..breaker.state` | enum (`closed` \| `half-open` \| `open`) | Circuit-breaker state |
| `psps..breaker.tripsInWindow` | integer | Breaker trips in the current 1h sliding window |
| `psps..breaker.lastTripAt` | ISO-8601 \| null | Most recent trip timestamp |
| `psps..breaker.lastResetAt` | ISO-8601 \| null | Most recent manual or auto reset |
### Error responses
| Status | When |
| ------ | --------------------------- |
| `403` | JWT / IP / geo guard failed |
### Example
```bash theme={null}
curl https://apiv3.droplinked.com/admin/psp/health \
-H "Authorization: Bearer "
```
### When to use
| Situation | Why this endpoint |
| ------------------------------- | -------------------------------------------------------------------- |
| Operator pings "is Bonum down?" | Read `psps.bonum.breaker.state` + `psps.bonum.probes.lastStatus` |
| Before bulk-reconciling Telr | Confirm `psps.telr.breaker.state === "closed"` to avoid wasted calls |
| Debugging high refund rate | Cross-reference with `GET /admin/dashboard/refund-rate` |
| Status-page automation | Poll every 60s; raise an incident when any breaker is `open` |
Probes are off by default for low-traffic PSPs (Paymob, Coinbase, PayPal) because the
synthetic call incurs vendor-side cost or burns sandbox quota. Enable only when the PSP
is in active use for the operating region.
## Related
* [Network Health KPIs](/api-reference/admin/kpi-dashboard) — orders / GMV / refund rate per PSP.
* [Bonum admin](/api-reference/admin/bonum) — reconcile + per-merchant config.
* [Telr admin](/api-reference/admin/telr) — reconcile.
# Telr Admin
Source: https://docs.droplinked.com/api-reference/admin/telr
Operator endpoint for Telr PSP — manual reconciliation for stuck PENDING orders.
The Telr admin surface currently exposes a single operator endpoint: **manual reconciliation**.
Telr's hosted-checkout sandbox occasionally drops the `txnref` webhook, leaving an order
in `PENDING` even after the customer completes payment. Reconcile pulls the canonical
state from Telr and writes through.
All admin endpoints below require:
* **JWT** with `role = SUPER_ADMIN`
* **`IpAllowlistGuard`** — caller IP must be in the operator allowlist
* **`GeoBlockGuard`** — caller geo must be permitted
Calls that miss any of the three return `403`.
## When to use
| Situation | Action |
| ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| Order is `PENDING` and Telr Dashboard shows the txn as `Authorised` or `Paid` | `POST /admin/telr/reconcile/:transactionId` |
| Customer reports a successful payment that doesn't show in their order history | First check Telr Dashboard, then reconcile |
| Bulk catch-up after a webhook outage | Loop the endpoint per stuck order; there is no batch variant — use the unified-transactions reconciliation cron instead |
## POST /admin/telr/reconcile/:transactionId
Manually reconciles a single Telr transaction. Bypasses any env-level reconciliation
gating (the endpoint is the recovery path of last resort).
Accepts **either**:
* The Telr `cartid` / `txnref` (issued at intent creation)
* Our internal order `ObjectId` (24-char hex)
The handler resolves the supplied ID to the `TelrTransaction`, calls Telr's
`/gateway/order.json` to refetch settlement state, then writes through to the order and
unified-transaction projection.
### Authentication
| Guard | Requirement |
| ------------ | --------------------------------- |
| JWT | Required, `role = SUPER_ADMIN` |
| IP allowlist | Caller IP in `ADMIN_IP_ALLOWLIST` |
| Geo | Country in `ADMIN_GEO_ALLOWLIST` |
### Path parameters
| Param | Type | Required | Description |
| --------------- | ------ | -------- | ---------------------------------------------------- |
| `transactionId` | string | Yes | Telr `cartid` / `txnref` **or** our order `ObjectId` |
### Request body
Empty — `POST` with no body.
### Response — 200 OK
```json theme={null}
{
"transactionId": "65f8a1b2c3d4e5f6a7b8c9d0",
"telrRef": "TELR-9921-CARTID-44A1B",
"merchantId": "65f8a1b2c3d4e5f6a7b8c9aa",
"previousStatus": "PENDING",
"currentStatus": "PAID",
"settledAt": "2026-06-04T12:34:56.789Z",
"amount": 12500,
"currency": "AED",
"reconciledVia": "manual-admin",
"orderUpdated": true,
"unifiedTransactionUpdated": true
}
```
### Error responses
| Status | When |
| ------ | ------------------------------------------------------------------ |
| `400` | `transactionId` is neither a Telr ref nor a valid order `ObjectId` |
| `403` | JWT missing / wrong role / IP or geo guard failed |
| `404` | No `TelrTransaction` matches the supplied ID |
| `502` | Telr `/gateway/order.json` returned a non-2xx |
| `503` | Telr API breaker is open |
### Example
```bash theme={null}
curl -X POST \
https://apiv3.droplinked.com/admin/telr/reconcile/65f8a1b2c3d4e5f6a7b8c9d0 \
-H "Authorization: Bearer "
```
Telr's sandbox at `secure.telr.com/gateway/order.json` (with `ivp_test=1`) **does not
guarantee webhook delivery**. The reconcile endpoint is the canonical recovery path for
both sandbox testing and prod webhook outages.
## Operational context
The reconcile endpoint exists because:
1. Telr's hosted checkout posts back to `txnref` callback URL **after** redirect — if the
customer closes the tab before redirect completes, the webhook is the only signal.
2. Telr's webhook retry policy is short (3 attempts within 1 hour). After that, the order
stays `PENDING` indefinitely unless an operator reconciles.
3. Sandbox runs (`ivp_test=1`) frequently skip webhook delivery; reconcile is the only
reliable way to confirm a test settled.
For systemic gaps (e.g., a sustained webhook outage), prefer the unified-transactions
reconciliation cron over looping this endpoint by hand.
## Related
* [Telr integration guide](/guides/integrations/telr) — payment flow, cohort matrix, sandbox setup.
* [Bonum admin endpoints](/api-reference/admin/bonum) — same reconcile pattern + per-merchant config CRUD.
* [PSP health](/api-reference/admin/psp-health) — confirm the Telr breaker is closed before reconciling at scale.
# Banner Image Generator
Source: https://docs.droplinked.com/api-reference/ai/banner-image-generator
Generate a 1920×512 banner image for a web3 business — uses Flux.1 Schnell with built-in retry. Returns base64 image.
### API Endpoint
* **URL**: `https://banner.droplinked.workers.dev/`
* **Method**: `POST`
* **Content-Type**: `application/json`
### Authentication
An API key is required for access. Contact Droplinked support to obtain your API key. Include it in the request headers as follows:
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked)
### Request Format
The API accepts a JSON payload with the following fields:
#### Request Body
| Field | Type | Required | Description |
| ------------- | ------ | -------- | ----------------------------------------------- |
| `description` | String | Yes | A brief or detailed description of the company. |
| `category` | String | Yes | The company’s category (e.g., "technology"). |
#### Example Request Body
```json theme={null}
{
"description": "droplinked , we are a company offering web3 based and onchain solution to businesses and indivisuals .",
"category": "technology"
}
```
### Response Format
* **Content-Type**: `text/plain`
* **Response**: A base64-encoded string representing the generated banner image.
#### Example Response
```
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB4AAAAMAAAACAYAAAD0eNT6...
```
### Guidelines for Banner Generation
The API operates as follows:
1. **Prompt Generation**:
* Calls an internal `text_llm` service with the `"banner_prompt"` command, passing `description` and `category`.
* Receives a cleaned, Flux-optimized prompt (e.g., "A sleek digital grid with glowing blockchain nodes, corporate banner style, high quality, detailed, professional, digital art").
2. **Image Generation**:
* Uses the Flux.1 Schnell model (`@cf/black-forest-labs/flux-1-schnell`).
* Fixed dimensions: 1920x512 pixels (divisible by 8 for model compatibility).
* Applies retries (up to 3) with exponential backoff and prompt simplification if generation fails.
3. **Output**:
* Converts the image to base64 using a robust utility function handling various input types.
* Returns only the base64 string, no additional metadata.
### JavaScript Example
Below is an example of how to use the Banner Image Generator API in JavaScript with the `fetch` API, including rendering the image:
```javascript theme={null}
// Set up headers
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("api-key", ""); // Replace with your Droplinked API key
// Define the request payload
const raw = JSON.stringify({
"description": "droplinked , we are a company offering web3 based and onchain solution to businesses and indivisuals .",
"category": "technology"
});
// Configure request options
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
// Make the API call
fetch("https://banner.droplinked.workers.dev/", requestOptions)
.then((response) => response.text())
.then((base64String) => {
// Display the image
const imgElement = document.createElement("img");
imgElement.src = `data:image/png;base64,${base64String}`;
document.body.appendChild(imgElement);
})
.catch((error) => console.error(error));
```
### Error Handling
The API provides JSON error responses with a debug header (`X-Debug-Version`):
* **405 Method Not Allowed**: If the method isn’t `POST`.
* Example: `{ "error": "Method not allowed. Use POST." }`
* **401 Unauthorized**: If the API key is missing or invalid.
* Example: `{ "error": "Authentication failed. Invalid API key." }`
* **400 Bad Request**: If `description` or `category` is missing.
* Example: `{ "error": "Missing required fields: 'description' and/or 'category'." }`
* **500 Internal Server Error**: If prompt generation, image creation, or base64 conversion fails.
* Example: `{ "error": "Failed to generate image after multiple attempts.", "details": "..." }`
* **503 Service Unavailable**: If the `text_llm` service fails.
* Example: `{ "error": "Failed to call text-llm worker.", "details": "..." }`
#### Example Error Output
```json theme={null}
{"error":"Authentication failed. API key is missing."}
```
### Best Practices
1. **Input Quality**: Provide a detailed `description` and relevant `category` for optimal banners.
2. **Image Usage**: Decode the base64 string client-side to render or save the image (e.g., as PNG).
3. **Retry Logic**: The API includes built-in retries; ensure your client handles final failures gracefully.
4. **Testing**: Test with various inputs to refine the output for your needs.
### Worker Code Insights
* **Base64 Conversion**: The `uint8ArrayToBase64` function handles multiple input types (Uint8Array, ArrayBuffer, arrays) and processes large data in chunks (8192 bytes).
* **Prompt Formatting**: The `formatFluxPrompt` function cleans the prompt and adds Flux-specific quality boosters (e.g., "high quality, professional").
* **Retries**: Implements exponential backoff (starting at 500ms) with prompt simplification across 3 attempts for reliability.
* **Debugging**: Includes detailed logging and a version header (`X-Debug-Version`) for diagnostics.
### Support
For assistance or to request an API key, contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Provide details such as your use case and expected request volume.
# Banner Prompt Generator
Source: https://docs.droplinked.com/api-reference/ai/banner-prompt-generator
Generate a concise prompt for Flux.1 Schnell to create a web3-themed banner with a futuristic style.
### API Endpoint
* **URL**: `https://text-tad.droplinked.workers.dev/`
* **Method**: `POST`
* **Content-Type**: `application/json`
### Authentication
An API key is required for access. Contact Droplinked support to obtain your API key. Include it in the request headers as follows:
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked)
### Request Format
The API accepts a JSON payload with the following fields:
#### Request Body
| Field | Type | Required | Description |
| -------------- | ------ | -------- | -------------------------------------------------------- |
| `command_name` | String | Yes | Must be set to `"banner_prompt"` for this functionality. |
| `description` | String | Yes | A brief or detailed description of the company. |
| `category` | String | Yes | The company’s category (e.g., "technology"). |
#### Example Request Body
```json theme={null}
{
"command_name": "banner_prompt",
"description": "droplinked , we are a company offering web3 based and onchain solution to businesses and indivisuals .",
"category": "technology"
}
```
### Response Format
* **Content-Type**: `text/plain`
* **Response**: A single, vivid sentence (under 200 characters) as the prompt for the FLUX.1 Schnell model, with no additional text or formatting.
#### Example Response
```
A sleek digital grid with glowing blockchain nodes and circuit patterns in a futuristic tech style.
```
### Guidelines for Banner Prompt Generation
The API generates prompts based on these rules:
1. **Design Focus**:
* Balanced layout with a focal point (e.g., blockchain link, digital node, network).
* Tied to web3 and onchain themes from the `description` and `category`.
2. **Style**:
* Sleek, modern, and futuristic.
* Incorporates elements like circuit patterns, geometric shapes, or digital grids.
3. **Content**:
* No text, logos, or brand names; uses visual metaphors for identity.
* Detailed yet clean, professional, and tech-savvy.
4. **Output**:
* Single sentence, under 200 characters.
* No extra text, formatting, or explanations—just the prompt.
### JavaScript Example
Below is an example of how to use the Banner Prompt Generator API in JavaScript with the `fetch` API:
```javascript theme={null}
// Set up headers
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("api-key", ""); // Replace with your Droplinked API key
// Define the request payload
const raw = JSON.stringify({
"command_name": "banner_prompt",
"description": "droplinked , we are a company offering web3 based and onchain solution to businesses and indivisuals .",
"category": "technology"
});
// Configure request options
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
// Make the API call
fetch("https://text-tad.droplinked.workers.dev/", requestOptions)
.then((response) => response.text())
.then((result) => console.log(result)) // Output: A sleek digital grid with glowing blockchain nodes...
.catch((error) => console.error(error));
```
### Error Handling
* **Invalid API Key**: Returns an error message (e.g., "Unauthorized").
* **Missing Fields**: If `command_name`, `description`, or `category` is missing, the API may return an error or fail silently.
* **Network Issues**: Handle errors in the `.catch` block of the `fetch` call.
#### Example Error Output
```
Unauthorized - Invalid API Key
```
### Best Practices
1. **Description Specificity**: Include key business traits in the `description` for relevant prompts.
2. **Category Relevance**: Ensure the `category` aligns with the business (e.g., "technology" for web3).
3. **Prompt Usage**: Feed the output directly into the FLUX.1 Schnell model for banner generation.
4. **Testing**: Test with varied inputs to refine the visual output.
### Support
For assistance or to request an API key, contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Provide details such as your use case and expected request volume.
# Domain Names Generator
Source: https://docs.droplinked.com/api-reference/ai/domain-names-generator
Suggest three `droplinked.io` subdomain options based on a business concept, returned as strict JSON.
### API Endpoint
* **URL**: `https://text-tad.droplinked.workers.dev/`
* **Method**: `POST`
* **Content-Type**: `application/json`
### Authentication
An API key is required for access. Contact Droplinked support to obtain your API key. Include it in the request headers as follows:
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked)
### Request Format
The API accepts a JSON payload with the following fields:
#### Request Body
| Field | Type | Required | Description |
| -------------- | ------ | -------- | -------------------------------------------------------- |
| `command_name` | String | Yes | Must be set to `"domain_names"` for this functionality. |
| `description` | String | Yes | A brief or detailed description of the shop or business. |
#### Example Request Body
```json theme={null}
{
"command_name": "domain_names",
"description": "A digital marketplace for NFT-based artwork with onchain verification."
}
```
### Response Format
* **Content-Type**: `text/plain`
* **Response**: A strict JSON object containing exactly three subdomain suggestions, with no additional text or characters.
#### Response Structure
```json theme={null}
{
"domain_names": ["subdomain1", "subdomain2", "subdomain3"]
}
```
#### Example Response
```json theme={null}
{
"domain_names": ["nftart", "onchainart", "digitalgallery"]
}
```
### Guidelines for Domain Name Generation
The API generates output based on these rules:
1. **Domain Names**:
* Exactly three unique subdomains reflecting the business concept from the `description`.
* Designed to be appended to `droplinked.io` (e.g., `droplinked.io/nftart`).
* Only the subdomain part is provided, without extensions or prefixes.
2. **Output**:
* Strict JSON format with only the `"domain_names"` key and a three-item array.
* No extra text, comments, or additional fields.
### JavaScript Example
Below is an example of how to use the Domain Names Generator API in JavaScript with the `fetch` API:
```javascript theme={null}
// Set up headers
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("api-key", ""); // Replace with your Droplinked API key
// Define the request payload
const raw = JSON.stringify({
"command_name": "domain_names",
"description": "A digital marketplace for NFT-based artwork with onchain verification."
});
// Configure request options
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
// Make the API call
fetch("https://text-tad.droplinked.workers.dev/", requestOptions)
.then((response) => response.text())
.then((result) => {
const data = JSON.parse(result);
console.log("Suggested Subdomains:", data.domain_names);
})
.catch((error) => console.error(error));
```
### Error Handling
* **Invalid API Key**: Returns an error message (e.g., "Unauthorized").
* **Missing Fields**: If `command_name` or `description` is missing, the API may return an error or fail silently.
* **Network Issues**: Handle errors in the `.catch` block of the `fetch` call.
#### Example Error Output
```
Unauthorized - Invalid API Key
```
### Best Practices
1. **Description Clarity**: Include key business elements (e.g., products, technology) for relevant subdomains.
2. **JSON Parsing**: Parse the response client-side to access the subdomain suggestions.
3. **Domain Usage**: Append the subdomains to `droplinked.io` for use (e.g., `droplinked.io/nftart`).
4. **Testing**: Test with different descriptions to explore subdomain options.
### Support
For assistance or to request an API key, contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Provide details such as your use case and expected request volume.
# Image Generation
Source: https://docs.droplinked.com/api-reference/ai/image-generation
Generate images from text prompts using Flux.1 Schnell — customizable style, background, aspect ratio. Returns a base64-encoded image.
### API Endpoint
* **URL**: `https://image-gen.droplinked.workers.dev/`
* **Method**: `POST`
* **Content-Type**: `application/json`
### Authentication
An API key is required for access. Contact Droplinked support to obtain your API key. Include it in the request headers as follows:
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked)
### Request Format
The API accepts a JSON payload with the following fields:
#### Request Body
| Field | Type | Required | Description |
| -------------- | ------ | -------- | ---------------------------------------------------------------------------- |
| `prompt` | String | Yes | The main description of the image content (e.g., "a man smoking cigar"). |
| `style` | String | No | The artistic style (e.g., "realistic", "cartoon"). Optional. |
| `background` | String | No | The background setting (e.g., "party", "forest"). Optional. |
| `aspect_ratio` | String | Yes | The image aspect ratio (e.g., "1x1", "16x9"). Supported values listed below. |
#### Supported Aspect Ratios
| Aspect Ratio | Dimensions (Width x Height) |
| ------------ | --------------------------- |
| `1x1` | 1024 x 1024 |
| `3x4` | 768 x 1024 |
| `4x3` | 1024 x 768 |
| `16x9` | 1920 x 1080 |
| `9x16` | 1080 x 1920 |
#### Example Request Body
```json theme={null}
{
"prompt": "a man smoking cigar",
"style": "realistic",
"background": "party",
"aspect_ratio": "1x1"
}
```
### Response Format
* **Content-Type**: `text/plain`
* **Response**: A base64-encoded string representing the generated image, which can be decoded and used as an image file (e.g., PNG).
#### Example Response
```
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABHNCSVQ...
```
### Guidelines for Image Generation
The API constructs the full prompt and generates the image as follows:
1. **Prompt Construction**:
* Starts with the `prompt` value.
* Appends `style` (if provided) as "in a \[style] style".
* Appends `background` (if provided) as "with a background of \[background]".
* Example: "a man smoking cigar in a realistic style with a background of party".
2. **Image Dimensions**: Set based on the specified `aspect_ratio` (see table above).
3. **Model**: Uses the Flux.1 Schnell model (`@cf/black-forest-labs/flux-1-schnell`) for generation.
4. **Output**: Returns a base64 string, processed in chunks to handle large data efficiently.
### JavaScript Example
Below is an example of how to use the Image Generation API in JavaScript with the `fetch` API, including decoding the base64 response:
```javascript theme={null}
// Set up headers
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("api-key", ""); // Replace with your Droplinked API key
// Define the request payload
const raw = JSON.stringify({
"prompt": "a man smoking cigar",
"style": "realistic",
"background": "party",
"aspect_ratio": "1x1"
});
// Configure request options
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
// Make the API call
fetch("https://image-gen.droplinked.workers.dev/", requestOptions)
.then((response) => response.text())
.then((base64String) => {
// Example: Display the image in an HTML img element
const imgElement = document.createElement("img");
imgElement.src = `data:image/png;base64,${base64String}`;
document.body.appendChild(imgElement);
})
.catch((error) => console.error(error));
```
### Error Handling
The API provides detailed error responses in JSON format:
* **405 Method Not Allowed**: If the request method isn’t `POST`.
* Example: `{ "error": "Method not allowed. Use POST." }`
* **401 Unauthorized**: If the API key is missing or invalid.
* Example: `{ "error": "Authentication failed. Invalid API key." }`
* **400 Bad Request**: If the JSON is invalid or required fields (`prompt`, `aspect_ratio`) are missing.
* Example: `{ "error": "Missing required fields: prompt or aspect_ratio." }`
* **400 Invalid Aspect Ratio**: If `aspect_ratio` isn’t supported.
* Example: `{ "error": "Invalid aspect ratio specified.", "received": "2x3" }`
* **500 Internal Server Error**: If image generation or database access fails.
* Example: `{ "error": "Failed to generate image.", "details": "..." }`
#### Example Error Output
```
{"error":"Authentication failed. API key is missing."}
```
### Best Practices
1. **Prompt Specificity**: Use clear, descriptive prompts for better results (e.g., "a woman in a red dress" vs. "a person").
2. **Optional Fields**: Include `style` and `background` only when needed to avoid cluttering the prompt.
3. **Aspect Ratio**: Choose an aspect ratio that matches your display needs.
4. **Base64 Handling**: Decode the response client-side to render or save the image (e.g., as PNG).
5. **Error Checking**: Handle errors gracefully with try-catch or `.catch` blocks.
### Worker Code Insights
* **Base64 Conversion**: The `uint8ArrayToBase64` function processes large image data in chunks (8192 bytes) to avoid stack overflow.
* **Authentication**: API keys are validated against a database (`env.key_DB`).
* **Model**: Uses Flux.1 Schnell for fast, high-quality image generation.
* **Debugging**: Responses include a header (`X-Debug-Version`) for version tracking.
### Support
For assistance or to request an API key, contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Provide details such as your use case and expected request volume.
# Image-to-Description Generator
Source: https://docs.droplinked.com/api-reference/ai/image-to-description-generator
Produce a 150–250 character product description paragraph from image-derived visual details.
### API Endpoint
* **URL**: `https://image-cap.droplinked.workers.dev/`
* **Method**: `POST`
* **Content-Type**: `application/json`
### Authentication
An API key is required to access the API. Contact Droplinked support to obtain your API key. Include it in the request headers as follows:
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked)
### Request Format
The API accepts a JSON payload with the following fields:
#### Request Body
| Field | Type | Required | Description |
| -------------- | ------ | -------- | -------------------------------------------------------------------- |
| `command_name` | String | Yes | Must be set to `"description_from_image"` for this functionality. |
| `imageUrl` | String | Yes | URL of the product image to analyze and generate a description from. |
#### Example Request Body
```json theme={null}
{
"command_name": "description_from_image",
"imageUrl": "https://dash.dev.lumai.ir/api/services/outputs/0194ccd6-e1c8-70fc-9083-4adef69bc112.jpg"
}
```
### Response Format
* **Content-Type**: `text/plain`
* **Response**: A single-paragraph string containing the product description, with no quotes, labels, or additional text.
#### Example Response
```
Step up your run with these Nike shoes in black mesh - Featuring a bold red swoosh and cushioned sole - Lightweight breathable design enhances comfort - Durable rubber outsole ensures lasting performance
```
### Guidelines for Generated Descriptions
The API adheres to these strict rules:
1. **Mode**: Operates in `GENERATE_DESCRIPTION_MODE: TRUE`, creating descriptions from visual data.
2. **Input**: Uses a product visual description extracted from the image (processed internally).
3. **Output**: Produces a single-paragraph description with:
* 150-250 characters.
* Brand name (if visible).
* Exact colors and patterns.
* Material and style details.
* Key features using hyphens (`-`) to separate points.
4. **Format**:
* Single flowing paragraph.
* No quotes or introductory/explanatory text.
* Describes only what is visible in the image.
5. **Restrictions**:
* No conversation, questions, or assumptions.
* No additional formatting or text beyond the description.
#### Example Transformation
* **Visual Description**: "Champion brand hoodie in mulled berry red color with tie-dye pattern throughout, featuring kangaroo pocket, drawstring hood, and ribbed cuffs"
* **Output Description**: `Elevate your casual style with this Champion tie-dye hoodie in rich mulled berry - Features a classic kangaroo pocket and adjustable drawstring hood - Premium cotton blend construction with tie-dye pattern throughout - Perfect blend of comfort and trendy streetwear appeal`
### JavaScript Example
Below is an example of how to use the Image-to-Description Generator API in JavaScript with the `fetch` API:
```javascript theme={null}
// Set up headers
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("api-key", ""); // Replace with your Droplinked API key
// Define the request payload
const raw = JSON.stringify({
"command_name": "description_from_image",
"imageUrl": "https://dash.dev.lumai.ir/api/services/outputs/0194ccd6-e1c8-70fc-9083-4adef69bc112.jpg"
});
// Configure request options
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
// Make the API call
fetch("https://image-cap.droplinked.workers.dev/", requestOptions)
.then((response) => response.text())
.then((result) => console.log(result)) // Output: e.g., Step up your run with these Nike shoes...
.catch((error) => console.error(error));
```
### Error Handling
* **Invalid API Key**: Returns an error message (e.g., "Unauthorized").
* **Missing Fields**: If `command_name` or `imageUrl` is missing, the API may return an error or fail silently.
* **Invalid Image URL**: If the URL is inaccessible or invalid, an error may occur.
* **Network Issues**: Handle errors in the `.catch` block of the `fetch` call.
#### Example Error Output
```
Unauthorized - Invalid API Key
```
### Best Practices
1. **Image Clarity**: Use high-resolution images to ensure accurate detail extraction.
2. **URL Validity**: Verify that the `imageUrl` is accessible and points to a valid image.
3. **Consistency**: Test with diverse products to ensure descriptions align with your catalog.
4. **SEO Benefits**: The descriptions naturally include keywords from visual details, enhancing searchability.
### Support
For assistance or to request an API key, contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Include details such as your use case and expected request volume.
# Image-to-Title Generator
Source: https://docs.droplinked.com/api-reference/ai/image-to-title-generator
Generate product titles from image-based visual descriptions — brand, color, pattern, product type — under 60 characters.
### API Endpoint
* **URL**: `https://image-cap.droplinked.workers.dev/`
* **Method**: `POST`
* **Content-Type**: `application/json`
### Authentication
Access to the API requires an API key. Contact Droplinked support to obtain your API key. Include it in the request headers as follows:
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked)
### Request Format
The API accepts a JSON payload with the following fields:
#### Request Body
| Field | Type | Required | Description |
| -------------- | ------ | -------- | -------------------------------------------------------------- |
| `command_name` | String | Yes | Must be set to `"title_from_image"` for this functionality. |
| `imageUrl` | String | Yes | URL of the product image to analyze and generate a title from. |
#### Example Request Body
```json theme={null}
{
"command_name": "title_from_image",
"imageUrl": "https://dash.dev.lumai.ir/api/services/outputs/0194ccd6-e1c8-70fc-9083-4adef69bc112.jpg"
}
```
### Response Format
* **Content-Type**: `text/plain`
* **Response**: A single-line string representing the generated product title, with no quotes, punctuation, or additional text.
#### Example Response
```
Adidas Blue Striped Running Shorts
```
### Guidelines for Generated Titles
The API follows these strict rules when generating titles:
1. **Mode**: Operates in `GENERATE_TITLE_MODE: TRUE`, transforming visual descriptions into titles.
2. **Input**: Expects a product visual description derived from the image (processed internally).
3. **Output**: Produces a single-line title only, with:
* Maximum 60 characters.
* Brand name (if identified in the image).
* Exact color and pattern as described.
* Product type included.
4. **Format**:
* No quotes or punctuation at the end.
* Concise and precise, matching the exact product depicted.
5. **Restrictions**:
* No conversational text, questions, or explanations.
* No additional formatting beyond the title itself.
#### Example Transformation
* **Visual Description**: "Champion brand hoodie in mulled berry red with tie-dye pattern, featuring front pocket and drawstring hood"
* **Output Title**: `Champion Mulled Berry Tie-Dye Pullover Hoodie`
### JavaScript Example
Below is an example of how to use the Image-to-Title Generator API in JavaScript with the `fetch` API:
```javascript theme={null}
// Set up headers
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("api-key", ""); // Replace with your Droplinked API key
// Define the request payload
const raw = JSON.stringify({
"command_name": "title_from_image",
"imageUrl": "https://dash.dev.lumai.ir/api/services/outputs/0194ccd6-e1c8-70fc-9083-4adef69bc112.jpg"
});
// Configure request options
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
// Make the API call
fetch("https://image-cap.droplinked.workers.dev/", requestOptions)
.then((response) => response.text())
.then((result) => console.log(result)) // Output: e.g., Adidas Blue Striped Running Shorts
.catch((error) => console.error(error));
```
### Error Handling
* **Invalid API Key**: Returns an error message (e.g., "Unauthorized").
* **Missing Fields**: If `command_name` or `imageUrl` is missing, the API may return an error or fail silently.
* **Invalid Image URL**: If the URL is inaccessible or invalid, an error may occur.
* **Network Issues**: Handle errors in the `.catch` block of the `fetch` call.
#### Example Error Output
```
Unauthorized - Invalid API Key
```
### Best Practices
1. **Image Quality**: Use clear, high-quality images for accurate visual description extraction.
2. **URL Accessibility**: Ensure the `imageUrl` is publicly accessible or properly authenticated.
3. **Consistency**: Test with various product images to ensure title accuracy aligns with your catalog.
4. **SEO Optimization**: The generated titles are inherently SEO-friendly due to precise keywords.
### Support
For API-related issues or to request an API key, contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Provide details such as your use case and expected request volume.
# Logo Image Generator
Source: https://docs.droplinked.com/api-reference/ai/logo-image-generator
Generate a 1024×1024 logo image from a company description using Flux.1 Schnell. Returns base64 image + prompt.
### API Endpoint
* **URL**: `https://logo.droplinked.workers.dev/`
* **Method**: `POST`
* **Content-Type**: `application/json`
### Authentication
An API key is required for access. Contact Droplinked support to obtain your API key. Include it in the request headers as follows:
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked)
### Request Format
The API accepts a JSON payload with the following field:
#### Request Body
| Field | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------------------------- |
| `description` | String | Yes | A brief or detailed description of the company or shop. |
#### Example Request Body
```json theme={null}
{
"description": "We are an online bookstore called Novel Nest, specializing in rare and classic literature with a modern and minimalist vibe. We want something sleek, refined, and timeless that represents our passion for literature."
}
```
### Response Format
* **Content-Type**: `application/json`
* **Response**: A JSON object containing:
* `base64_image`: A base64-encoded string of the generated logo image.
* `generated_prompt`: The text prompt used to create the image.
#### Example Response
```json theme={null}
{
"base64_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6...",
"generated_prompt": "A sleek, modern, and minimalist logo for Novel Nest, specializing in rare and classic literature. Subtle nod to timeless books, refined typography, and a clean design representing passion for reading."
}
```
### Guidelines for Logo Generation
The API operates as follows:
1. **Prompt Generation**:
* Internally calls a service (bound as `env.ratatatata`) with the `"logo_prompt"` command and the provided `description`.
* Generates a concise text prompt capturing the brand’s identity and style.
2. **Image Generation**:
* Uses the Flux.1 Schnell model (`@cf/black-forest-labs/flux-1-schnell`).
* Fixed dimensions: 1024x1024 pixels.
3. **Output**:
* Returns the base64-encoded image and the prompt used.
* CORS-enabled for cross-origin requests.
### JavaScript Example
Below is an example of how to use the Logo Image Generator API in JavaScript with the `fetch` API, including rendering the image:
```javascript theme={null}
// Set up headers
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("api-key", ""); // Replace with your Droplinked API key
// Define the request payload
const raw = JSON.stringify({
"description": "We are an online bookstore called Novel Nest, specializing in rare and classic literature with a modern and minimalist vibe. We want something sleek, refined, and timeless that represents our passion for literature."
});
// Configure request options
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
// Make the API call
fetch("https://logo.droplinked.workers.dev/", requestOptions)
.then((response) => response.text())
.then((result) => {
const data = JSON.parse(result);
// Display the image
const imgElement = document.createElement("img");
imgElement.src = `data:image/png;base64,${data.base64_image}`;
document.body.appendChild(imgElement);
console.log("Generated Prompt:", data.generated_prompt);
})
.catch((error) => console.error(error));
```
### Error Handling
The API provides JSON error responses with CORS headers:
* **405 Method Not Allowed**: If the request method isn’t `POST`.
* Example: `{ "error": "Method not allowed" }`
* **401 Unauthorized**: If the API key is missing.
* Example: `{ "error": "API key is required in headers as 'api-key'" }`
* **400 Bad Request**: If `description` is missing.
* Example: `{ "error": "Description is required" }`
* **500 Internal Server Error**: If prompt generation or image creation fails.
* Example: `{ "error": "Failed to generate image" }`
* **503 Service Unavailable**: If the internal text-tad worker is unreachable.
* Example: `{ "error": "Failed to connect to text-tad worker", "details": "..." }`
#### Example Error Output
```json theme={null}
{"error":"API key is required in headers as 'api-key'"}
```
### Best Practices
1. **Description Quality**: Provide a detailed description with brand name, style, and key traits for optimal logo output.
2. **Image Rendering**: Decode the `base64_image` client-side to display or save the logo (e.g., as PNG).
3. **CORS**: The API supports cross-origin requests, making it versatile for web applications.
4. **Error Handling**: Check response status and parse errors to manage failures gracefully.
### Worker Code Insights
* **CORS**: Configured with permissive headers (`Access-Control-Allow-Origin: *`) for broad compatibility.
* **Service Binding**: Uses `env.ratatatata` to fetch the logo prompt internally, passing the API key and description.
* **Image Model**: Employs Flux.1 Schnell for high-quality, fast logo generation at 1024x1024.
* **Debugging**: Includes console logs for diagnostics (e.g., environment binding checks).
### Support
For assistance or to request an API key, contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Provide details such as your use case and expected request volume.
# Logo Prompt Generator
Source: https://docs.droplinked.com/api-reference/ai/logo-prompt-generator
Generate a text-to-image prompt tailored for logo creation from a company description.
### API Endpoint
* **URL**: `https://text-tad.droplinked.workers.dev/`
* **Method**: `POST`
* **Content-Type**: `application/json`
### Authentication
An API key is required for access. Contact Droplinked support to obtain your API key. Include it in the request headers as follows:
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked)
### Request Format
The API accepts a JSON payload with the following fields:
#### Request Body
| Field | Type | Required | Description |
| -------------- | ------ | -------- | ------------------------------------------------------- |
| `command_name` | String | Yes | Must be set to `"logo_prompt"` for this functionality. |
| `description` | String | Yes | A brief or detailed description of the company or shop. |
#### Example Request Body
```json theme={null}
{
"command_name": "logo_prompt",
"description": "droplinked , we are a company offering web3 based and onchain solution to businesses and indivisuals ."
}
```
### Response Format
* **Content-Type**: `text/plain`
* **Response**: A raw text string representing the logo prompt, with no JSON, formatting, or additional text.
#### Example Response
```
A futuristic logo for droplinked, offering web3 and onchain solutions. Bold design with blockchain-inspired elements and a sleek, modern style for businesses and individuals.
```
### Guidelines for Logo Prompt Generation
The API generates prompts based on these principles:
1. **Input Processing**: Extracts key details from the `description` (e.g., company name, industry, style preferences).
2. **Output Design**:
* Concise and clear, tailored for text-to-image AI.
* Incorporates brand identity, imagery, and styling cues.
* Focuses on clarity and relevance to the company/shop.
3. **Restrictions**:
* No extra text (e.g., no greetings, explanations, or JSON).
* No code blocks, disclaimers, or labels.
* Pure text prompt only.
#### Example Transformation
* **Input Description**: "We are an online bookstore called Novel Nest, specializing in rare and classic literature with a modern and minimalist vibe. We want something sleek, refined, and timeless that represents our passion for literature."
* **Output Prompt**: `A sleek, modern, and minimalist logo for Novel Nest, specializing in rare and classic literature. Subtle nod to timeless books, refined typography, and a clean design representing passion for reading.`
### JavaScript Example
Below is an example of how to use the Logo Prompt Generator API in JavaScript with the `fetch` API:
```javascript theme={null}
// Set up headers
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("api-key", ""); // Replace with your Droplinked API key
// Define the request payload
const raw = JSON.stringify({
"command_name": "logo_prompt",
"description": "droplinked , we are a company offering web3 based and onchain solution to businesses and indivisuals ."
});
// Configure request options
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
// Make the API call
fetch("https://text-tad.droplinked.workers.dev/", requestOptions)
.then((response) => response.text())
.then((result) => console.log(result)) // Output: A futuristic logo for droplinked...
.catch((error) => console.error(error));
```
### Error Handling
* **Invalid API Key**: Returns an error message (e.g., "Unauthorized").
* **Missing Fields**: If `command_name` or `description` is missing, the API may return an error or fail silently.
* **Network Issues**: Handle errors in the `.catch` block of the `fetch` call.
#### Example Error Output
```
Unauthorized - Invalid API Key
```
### Best Practices
1. **Description Detail**: Provide a clear description with key brand elements (e.g., name, industry, vibe) for optimal results.
2. **Prompt Usage**: Feed the output directly into a text-to-image AI tool for logo generation.
3. **Testing**: Experiment with different descriptions to refine the generated prompts.
4. **Integration**: Use in workflows to automate logo ideation for businesses or clients.
### Support
For assistance or to request an API key, contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Provide details such as your use case and expected request volume.
# NSFW Detector
Source: https://docs.droplinked.com/api-reference/ai/nsfw-detector
Analyze text for Not Safe For Work content (explicit language, hate speech) — returns a boolean.
### API Endpoint
* **URL**: `https://text-tad.droplinked.workers.dev/`
* **Method**: `POST`
* **Content-Type**: `application/json`
### Authentication
Access to the API requires an API key. Contact Droplinked support to obtain your API key. Include it in the request headers as follows:
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked)
### Request Format
The API accepts a JSON payload with the following fields:
#### Request Body
| Field | Type | Required | Description |
| -------------- | ------ | -------- | -------------------------------------------------------- |
| `command_name` | String | Yes | Must be set to `"NSFW_detector"` for this functionality. |
| `text` | String | Yes | The text to be analyzed for NSFW content. |
#### Example Request Body
```json theme={null}
{
"command_name": "NSFW_detector",
"text": "Hey, let’s catch up later. I have some news i want to put my big stick in your mouth!"
}
```
### Response Format
* **Content-Type**: `text/plain`
* **Response**: A plain boolean value (`true` or `false`) indicating the presence of NSFW content, with no additional text or formatting.
#### Example Responses
* For NSFW content: `true`
* For clean or mildly affectionate text: `false`
### Guidelines for NSFW Detection
The API follows these rules:
1. **Definition of NSFW**: Flags text containing:
* Explicit language or strong sexual content.
* Graphic violence.
* Hate speech or inappropriate material.
2. **Exclusions**: Does not flag:
* Mildly affectionate or romantic phrases (e.g., "I would like to kiss you") unless explicitly inappropriate.
* General communication without NSFW elements.
3. **Output**: Returns only:
* `true` if NSFW content is detected.
* `false` if the text is clean or non-explicit.
4. **Restrictions**:
* No explanations, labels, or additional text in the response.
* Strict boolean output.
#### Example Transformations
* **Input**: "This video is full of explicit adult scenes!" → **Output**: `true`
* **Input**: "Hey, let’s catch up later. I have some news!" → **Output**: `false`
* **Input**: "I would like to kiss you." → **Output**: `false`
* **Input**: "I want to do explicit things to you." → **Output**: `true`
### JavaScript Example
Below is an example of how to use the NSFW Detector API in JavaScript with the `fetch` API:
```javascript theme={null}
// Set up headers
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("api-key", ""); // Replace with your Droplinked API key
// Define the request payload
const raw = JSON.stringify({
"command_name": "NSFW_detector",
"text": "Hey, let’s catch up later. I have some news i want to put my big stick in your mouth!"
});
// Configure request options
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
// Make the API call
fetch("https://text-tad.droplinked.workers.dev/", requestOptions)
.then((response) => response.text())
.then((result) => console.log(result)) // Output: true
.catch((error) => console.error(error));
```
### Error Handling
* **Invalid API Key**: Returns an error message (e.g., "Unauthorized").
* **Missing Fields**: If `command_name` or `text` is missing, the API may return an error or fail silently.
* **Network Issues**: Handle errors in the `.catch` block of the `fetch` call.
#### Example Error Output
```
Unauthorized - Invalid API Key
```
### Best Practices
1. **Text Clarity**: Provide clear, complete text inputs for accurate detection.
2. **Context Awareness**: Understand that the API focuses on explicit content, not implied meanings.
3. **Moderation Workflow**: Integrate the API into content pipelines to filter NSFW material efficiently.
4. **Testing**: Test with various inputs to ensure alignment with your moderation standards.
### Support
For assistance or to request an API key, contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Provide details such as your use case and expected request volume.
# NSFW Image Detector
Source: https://docs.droplinked.com/api-reference/ai/nsfw-image-detector
Evaluate image descriptions for NSFW elements (nudity, suggestive content) — returns a boolean.
### API Endpoint
* **URL**: `https://text-tad.droplinked.workers.dev/`
* **Method**: `POST`
* **Content-Type**: `application/json`
### Authentication
An API key is required for access. Contact Droplinked support to obtain your API key. Include it in the request headers as follows:
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked)
### Request Format
The API accepts a JSON payload with the following fields:
#### Request Body
| Field | Type | Required | Description |
| -------------- | ------ | -------- | -------------------------------------------------------------- |
| `command_name` | String | Yes | Must be set to `"NSFW_image_detector"` for this functionality. |
| `ImageUrl` | String | Yes | URL of the image to analyze for NSFW content. |
#### Example Request Body
```json theme={null}
{
"command_name": "NSFW_image_detector",
"ImageUrl": "https://upload-file-droplinked.s3.amazonaws.com/thumbnail/1734440601390-womens-cropped-sweatshirt-brick-front-676176913a8d1.png"
}
```
### Response Format
* **Content-Type**: `text/plain`
* **Response**: A plain boolean value (`true` or `false`) indicating the presence of NSFW content, with no additional text or formatting.
#### Example Responses
* For NSFW content: `true`
* For safe content: `false`
### Guidelines for NSFW Detection
The API follows these criteria based on an internally generated textual description of the image:
1. **NSFW Indicators** (returns `true` if detected):
* **Nudity or Partial Nudity**: Exposed skin, breasts, genitals, or buttocks.
* **Revealing or Transparent Clothing**: Sheer lingerie, underwear, or excessive exposure.
* **Sexually Suggestive Content**: Provocative poses, intimate settings, or adult themes.
* **Explicit Symbols or Contexts**: Adult toys, suggestive gestures, or explicit imagery.
2. **Safe Content** (returns `false`):
* Descriptions lacking the above indicators (e.g., fully clothed individuals, neutral settings).
3. **Rules**:
* Analyzes only the generated description from the image.
* Avoids false positives by requiring clear NSFW elements.
* Returns a plain boolean (`true` or `false`) with no explanations.
#### Example Scenarios
* **Generated Description**: "A woman in a sheer bodysuit, partially exposing her chest in an intimate setting" → **Output**: `true`
* **Generated Description**: "A woman in a brick-colored cropped sweatshirt against a plain background" → **Output**: `false`
### JavaScript Example
Below is an example of how to use the NSFW Image Detector API in JavaScript with the `fetch` API:
```javascript theme={null}
// Set up headers
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("api-key", ""); // Replace with your Droplinked API key
// Define the request payload
const raw = JSON.stringify({
"command_name": "NSFW_image_detector",
"ImageUrl": "https://upload-file-droplinked.s3.amazonaws.com/thumbnail/1734440601390-womens-cropped-sweatshirt-brick-front-676176913a8d1.png"
});
// Configure request options
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
// Make the API call
fetch("https://text-tad.droplinked.workers.dev/", requestOptions)
.then((response) => response.text())
.then((result) => console.log(result)) // Output: e.g., false
.catch((error) => console.error(error));
```
### Error Handling
* **Invalid API Key**: Returns an error message (e.g., "Unauthorized").
* **Missing Fields**: If `command_name` or `ImageUrl` is missing, the API may return an error or fail silently.
* **Invalid Image URL**: If the URL is inaccessible or invalid, an error may occur.
* **Network Issues**: Handle errors in the `.catch` block of the `fetch` call.
#### Example Error Output
```
Unauthorized - Invalid API Key
```
### Best Practices
1. **Image Quality**: Use clear, high-resolution images for accurate description generation.
2. **URL Accessibility**: Ensure the `ImageUrl` is publicly accessible or properly authenticated.
3. **Moderation Integration**: Embed the API in workflows to filter NSFW images efficiently.
4. **Testing**: Test with a variety of images to confirm detection aligns with your standards.
### Support
For assistance or to request an API key, contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Provide details such as your use case and expected request volume.
# AI Endpoints
Source: https://docs.droplinked.com/api-reference/ai/overview
A suite of AI-powered tools for e-commerce content, branding, and moderation — title and description enhancement, image and logo generation, NSFW detection, and more.
## Overview of AI Tools Documentation
This documentation provides a comprehensive guide to a suite of AI-powered tools designed to enhance e-commerce, content creation, and branding workflows, particularly for businesses leveraging web3 and onchain technologies. Each tool is accessible via a dedicated API endpoint, requiring authentication with an API key (see Authentication). Below is a summary of the tools covered:
* **Product Title Enhancement**: Optimizes product titles for SEO and engagement, ensuring concise, keyword-rich outputs within 60 characters, with optional emojis for appeal.
* **Product Description Generator**: Creates engaging product descriptions in a bullet-point format, incorporating SEO keywords and benefit-driven language, limited to 3-5 points with 1-2 relevant emojis.
* **Image-to-Title Generator**: Generates precise product titles from image-based visual descriptions, focusing on brand, color, pattern, and product type, with a maximum of 60 characters.
* **Image-to-Description Generator**: Produces detailed product descriptions from image-derived details, formatted as a single paragraph with 150-250 characters, focusing on exact visual elements.
* **NSFW Detector**: Analyzes text for Not Safe For Work (NSFW) content, such as explicit language or hate speech, returning a boolean (`true` for NSFW, `false` for safe).
* **NSFW Image Detector**: Evaluates image descriptions for NSFW elements (e.g., nudity, suggestive content), returning a boolean (`true` for NSFW, `false` for safe).
* **Image Generator**: Creates images based on text prompts using the Flux.1 Schnell model, allowing customization of style, background, and aspect ratio, returning a base64-encoded image.
* **Logo Prompt Generator**: Generates text prompts for logo creation based on a company description, tailored for text-to-image AI tools, focusing on brand identity and style.
* **Logo Image Generator**: Produces logo images by generating a prompt from a description and rendering a 1024x1024 image via Flux.1 Schnell, returning a base64 image and the prompt.
* **Shop Info Generator**: Provides three suggestions each for shop names, subdomains (for `droplinked.io`), and descriptions based on a business concept, returned in a structured JSON format.
* **Shop Names Generator**: Suggests three shop names tailored to the business description, output in a strict JSON format.
* **Domain Names Generator**: Offers three subdomain suggestions for `droplinked.io` based on the business concept, returned in a strict JSON format.
* **Shop Descriptions Generator**: Creates three concise and engaging shop descriptions from a business description, output in a strict JSON format.
* **Banner Prompt Generator**: Generates a concise text prompt for the Flux.1 Schnell model to create a banner, focusing on web3 themes with a futuristic style, avoiding text or logos.
* **Banner Image Generator**: Produces a 1920x512 banner image by generating a prompt and using Flux.1 Schnell, returning a base64-encoded image with retry logic for reliability.
Each API is designed for seamless integration into workflows, offering robust error handling, CORS support where applicable, and detailed usage examples in JavaScript. For access or support, contact Droplinked support to obtain an API key and discuss your use case.
## Endpoints
* [Product Title Enhancement](/api-reference/ai/product-title-enhancement)
* [Product Description Generator](/api-reference/ai/product-description-generator)
* [Image-to-Title Generator](/api-reference/ai/image-to-title-generator)
* [Image-to-Description Generator](/api-reference/ai/image-to-description-generator)
* [NSFW Detector](/api-reference/ai/nsfw-detector)
* [NSFW Image Detector](/api-reference/ai/nsfw-image-detector)
* [Image Generation](/api-reference/ai/image-generation)
* [Logo Image Generator](/api-reference/ai/logo-image-generator)
* [Logo Prompt Generator](/api-reference/ai/logo-prompt-generator)
* [Shop Names Generator](/api-reference/ai/shop-names-generator)
* [Domain Names Generator](/api-reference/ai/domain-names-generator)
* [Shop Descriptions Generator](/api-reference/ai/shop-descriptions-generator)
* [Banner Image Generator](/api-reference/ai/banner-image-generator)
* [Banner Prompt Generator](/api-reference/ai/banner-prompt-generator)
* [Segmind Logo generation](/api-reference/ai/segmind-logo-generation)
* [Segmind Banner Generation](/api-reference/ai/segmind-banner-generation)
# Product Description Generator
Source: https://docs.droplinked.com/api-reference/ai/product-description-generator
Generate engaging product descriptions in a bullet-point format with SEO keywords and benefit-driven language.
### API Endpoint
* **URL**: `https://text-tad.droplinked.workers.dev/`
* **Method**: `POST`
* **Content-Type**: `application/json`
### Authentication
To use the API, an API key is required. Contact Droplinked support to obtain your API key. Include it in the request headers as follows:
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked)
### Request Format
The API accepts a JSON payload with the following fields:
#### Request Body
| Field | Type | Required | Description |
| -------------- | ------ | -------- | ---------------------------------------------------------- |
| `command_name` | String | Yes | Must be set to `"description"` for description generation. |
| `title` | String | Yes | The product title to base the description on. |
| `description` | String | No | Optional existing description to enhance (can be empty). |
| `tone` | String | Yes | The desired tone (e.g., "wellness-focused", "tech-savvy"). |
#### Example Request Body
```json theme={null}
{
"command_name": "description",
"title": "Wireless Bluetooth Headphones",
"description": "",
"tone": "tech-savvy"
}
```
### Response Format
* **Content-Type**: `text/plain`
* **Response**: A plain text string containing the enhanced description, formatted with:
* 3-5 bullet points using hyphens (`-`)
* 1-2 relevant emojis maximum
* No JSON wrapper, Markdown, or additional explanations
#### Example Response
```
Elevate your audio game with cutting-edge tech 🎧
- Seamless wireless Bluetooth 5.0 connectivity
- Noise-canceling for immersive sound
- 20-hour battery life for all-day use
- Lightweight design with comfy earpads
```
### Guidelines for Generated Descriptions
The API adheres to these rules:
1. **Direct Output**: Only the enhanced description is returned—no JSON, formatting markers, or explanations.
2. **Structure**: Descriptions consist of:
* An introductory sentence (optional, based on input).
* 3-5 bullet points starting with hyphens (`-`).
3. **Content**:
* Incorporates SEO keywords derived from the `title` and `description`.
* Uses clear, benefit-driven language tailored to the specified `tone`.
* Includes 1-2 relevant emojis maximum.
4. **Restrictions**:
* No JSON syntax or wrappers.
* No Markdown (e.g., `**`, `#`).
* No technical notes or extraneous text.
### JavaScript Example
Below is an example of how to use the Product Description Generator API in JavaScript with the `fetch` API:
```javascript theme={null}
// Set up headers
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("api-key", ""); // Replace with your Droplinked API key
// Define the request payload
const raw = JSON.stringify({
"command_name": "description",
"title": "Wireless Bluetooth Headphones",
"description": "",
"tone": "tech-savvy"
});
// Configure request options
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
// Make the API call
fetch("https://text-tad.droplinked.workers.dev/", requestOptions)
.then((response) => response.text())
.then((result) => console.log(result)) // Output: See example response above
.catch((error) => console.error(error));
```
### Error Handling
* **Invalid API Key**: Returns an error message (e.g., "Unauthorized").
* **Missing Required Fields**: If `command_name`, `title`, or `tone` is missing, the API may return an error or unexpected output.
* **Network Errors**: Handle errors in the `.catch` block of the `fetch` call.
#### Example Error Output
```
Unauthorized - Invalid API Key
```
### Best Practices
1. **Tone Selection**: Choose a tone that matches your target audience (e.g., "casual", "luxury", "tech-savvy").
2. **Input Description**: Provide an optional `description` to give the API more context, though it works fine with just a `title`.
3. **Consistency**: Use consistent tones across related products for brand coherence.
4. **Testing**: Experiment with different titles and tones to refine the output.
### Support
For assistance with the API or to request an API key, contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Include details like your use case and expected request volume.
# Product Title Enhancement
Source: https://docs.droplinked.com/api-reference/ai/product-title-enhancement
Optimize product titles for SEO and engagement — concise, keyword-rich output under 60 characters with optional emojis.
### Overview
The Title Enhancement API is a powerful tool designed to optimize and enhance product titles for better engagement and search engine optimization (SEO). It transforms plain product titles into concise, keyword-rich, and appealing versions while adhering to specific guidelines. This API is ideal for e-commerce platforms, content creators, and marketers looking to improve product visibility and click-through rates.
### API Endpoint
* **URL**: `https://text-tad.droplinked.workers.dev/`
* **Method**: `POST`
* **Content-Type**: `application/json`
### Authentication
To access the API, you need an API key. Contact Droplinked support to obtain your API key. Include the API key in the request headers as follows:
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked)
### Request Format
The API accepts a JSON payload with the following fields:
#### Request Body
| Field | Type | Required | Description |
| -------------- | ------ | -------- | -------------------------------------------------------------------- |
| `command_name` | String | Yes | Must be set to `"title"` for title enhancement. |
| `title` | String | Yes | The original product title to be enhanced. |
| `tone` | String | Yes | The desired tone for the title (e.g., "professional", "tech-savvy"). |
#### Example Request Body
```json theme={null}
{
"command_name": "title",
"title": "Wireless Bluetooth Headphones",
"tone": "tech-savvy"
}
```
### Response Format
* **Content-Type**: `text/plain`
* **Response**: A single-line string representing the enhanced title, with no quotation marks or additional explanations.
#### Example Response
```
Tech-Savvy Wireless Bluetooth Headphones 🎧
```
### Guidelines for Enhanced Titles
The API follows these rules when generating enhanced titles:
1. **Character Limit**: The output title will not exceed 60 characters.
2. **SEO Optimization**: Relevant keywords are naturally incorporated to improve searchability.
3. **Tone Adaptation**: The title reflects the specified tone (e.g., "professional", "tech-savvy").
4. **Emojis**: Relevant emojis are added only when they enhance appeal and align with the tone.
5. **Clean Output**: No unnecessary symbols (e.g., `**`, `--`) or extraneous text are included.
### JavaScript Example
Below is an example of how to use the Title Enhancement API in JavaScript using the `fetch` API:
```javascript theme={null}
// Set up headers
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("api-key", ""); // Replace with your Droplinked API key
// Define the request payload
const raw = JSON.stringify({
"command_name": "title",
"title": "Wireless Bluetooth Headphones",
"tone": "tech-savvy"
});
// Configure request options
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
// Make the API call
fetch("https://text-tad.droplinked.workers.dev/", requestOptions)
.then((response) => response.text())
.then((result) => console.log(result)) // Output: Tech-Savvy Wireless Bluetooth Headphones 🎧
.catch((error) => console.error(error));
```
### Error Handling
* **Invalid API Key**: Returns an error message (e.g., "Unauthorized").
* **Missing Fields**: If `command_name`, `title`, or `tone` is missing, the API may return an error or unexpected behavior.
* **Network Issues**: Handle errors in the `.catch` block of the `fetch` call.
#### Example Error Output
```
Unauthorized - Invalid API Key
```
### Best Practices
1. **Tone Selection**: Choose a tone that aligns with your brand or audience (e.g., "casual", "professional", "fun").
2. **Title Length**: Provide input titles that allow room for enhancement within the 60-character limit.
3. **Testing**: Test with various titles and tones to ensure the output meets your needs.
### Support
For issues with the API or to request an API key, contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Provide details such as your use case and expected volume of requests.
# Segmind Banner Generation
Source: https://docs.droplinked.com/api-reference/ai/segmind-banner-generation
Generate banners via Segmind's image generation pipeline, with model + style controls.
### Overview
The Segmind Banner Generation API generates a banner image by first creating a text prompt using the existing Banner Prompt Generator, then submitting it to Segmind's API for image generation. It offers two options: a banner with a logo or a banner without a logo. The process concludes with polling for the final image URL, making this tool ideal for creating professional, web3-themed banners for businesses.
### API Endpoints
* **Prompt Generation**:
* **URL**: `https://text-tad.droplinked.workers.dev/`
* **Method**: `POST`
* **Segmind Workflow Submission**:
* With Logo: `https://api.segmind.com/workflows/67792f3899c1698a193130be-v2`
* Without Logo: `https://api.segmind.com/workflows/67d66d953fccc7a19c84cfa1-v1`
* **Method**: `POST`
* **Segmind Polling**:
* **URL**: `https://api.segmind.com/workflows/request/[request_id]`
* **Method**: `GET`
* **Content-Type**: `application/json`
### Authentication
This API requires two API keys:
1. **For Prompt Generation** (via `text-tad.droplinked.workers.dev`):
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked).
2. **For Segmind API** (via `api.segmind.com`):
* **Header**: `x-api-key`
* **Value**: `` (your Segmind account API key).
Contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com) to obtain these API keys.
### Request Format
The process involves three steps:
#### Step 1: Generate Banner Prompt
This step uses the existing Banner Prompt Generator API to create a text prompt.
**Request Body**
| Field | Type | Required | Description |
| -------------- | ------ | -------- | ----------------------------------------------- |
| `command_name` | String | Yes | Must be set to `"banner_prompt"`. |
| `description` | String | Yes | A brief or detailed description of the company. |
| `category` | String | Yes | The company’s category (e.g., "technology"). |
**Example Request Body**
```json theme={null}
{
"command_name": "banner_prompt",
"description": "droplinked , we are a company offering web3 based and onchain solution to businesses and indivisuals .",
"category": "technology"
}
```
**Response**
A JSON object containing the generated prompt.
**Example Response**
```json theme={null}
{"success":true,"output":"A futuristic digital node amidst circuit patterns and geometric shapes, symbolizing seamless connectivity and innovative web3 solutions."}
```
***
#### Step 2: Submit to Segmind Workflow
Submit the generated prompt to Segmind's API to start banner generation. There are two options: with a logo or without a logo.
**Option 1: Banner With Logo**
**Request Body**
| Field | Type | Required | Description |
| ------------------- | ------ | -------- | ----------------------------------------------- |
| `input_description` | String | Yes | The generated banner prompt from Step 1. |
| `input_seed` | String | Yes | A seed value for generation (e.g., "1"). |
| `input_logo` | String | Yes | URL of the logo image to include in the banner. |
**Example Request Body**
```json theme={null}
{
"input_description": "A blockchain node radiates energy amidst a grid of interconnected circuits, symbolizing seamless web3 connectivity",
"input_seed": "1",
"input_logo": "https://upload-file-droplinked.s3.amazonaws.com/860b369c4ceefd8b643713e955f487423b080e424cda8379ff0860c4bd92395f_small.jpg"
}
```
**Endpoint**
* **URL**: `https://api.segmind.com/workflows/67792f3899c1698a193130be-v2`
**Option 2: Banner Without Logo**
**Request Body**
| Field | Type | Required | Description |
| ------------------- | ------ | -------- | ---------------------------------------- |
| `input_description` | String | Yes | The generated banner prompt from Step 1. |
| `input_seed` | String | Yes | A seed value for generation (e.g., "1"). |
**Example Request Body**
```json theme={null}
{
"input_description": "A blockchain node radiates energy amidst a grid of interconnected circuits, symbolizing seamless web3 connectivity",
"input_seed": "1"
}
```
**Endpoint**
* **URL**: `https://api.segmind.com/workflows/67d66d953fccc7a19c84cfa1-v1`
**Response (Both Options)**
A JSON object with a polling URL and request ID.
**Example Response**
```json theme={null}
{
"message": "Your request with this id 68fe486045f6afa20e521094f20192ab currently in queue. Please be patient while we process it.",
"poll_url": "https://api.segmind.com/workflows/request/68fe486045f6afa20e521094f20192ab",
"request_id": "68fe486045f6afa20e521094f20192ab",
"status": "QUEUED"
}
```
***
#### Step 3: Poll for Final Image URL
Use the `poll_url` from Step 2 to check the status and retrieve the final image URL.
**Request**
* **Method**: `GET`
* **URL**: The `poll_url` from the Step 2 response (e.g., `https://api.segmind.com/workflows/request/68fe486045f6afa20e521094f20192ab`).
* **Headers**: Include the Segmind API key (`x-api-key`).
**Response**
A JSON object with the status and final image URL once completed.
**Example Response**
```json theme={null}
{
"output": "[{\"keyname\": \"output_banner\", \"value\": {\"data\": \"https://images.segmind.com/outputs/dbeae453-9a69-4622-83bf-8f4bd52eba49.png\", \"type\": \"image\"}}]",
"status": "COMPLETED"
}
```
### JavaScript Example
Below is an example of how to use the Segmind Banner Generation API in JavaScript with the `fetch` API, supporting both banner options:
```javascript theme={null}
// Step 1: Generate the banner prompt
const promptHeaders = new Headers();
promptHeaders.append("Content-Type", "application/json");
promptHeaders.append("api-key", ""); // Replace with your API key
const promptRaw = JSON.stringify({
"command_name": "banner_prompt",
"description": "droplinked , we are a company offering web3 based and onchain solution to businesses and indivisuals .",
"category": "technology"
});
const promptRequestOptions = {
method: "POST",
headers: promptHeaders,
body: promptRaw,
redirect: "follow"
};
fetch("https://text-tad.droplinked.workers.dev/", promptRequestOptions)
.then((response) => response.json())
.then((promptResult) => {
const bannerPrompt = promptResult.output;
// Step 2: Submit to Segmind (Option: With Logo)
const segmindHeaders = new Headers();
segmindHeaders.append("x-api-key", ""); // Replace with your Segmind API key
segmindHeaders.append("Content-Type", "application/json");
const segmindRaw = JSON.stringify({
"input_description": bannerPrompt,
"input_seed": "1",
"input_logo": "https://upload-file-droplinked.s3.amazonaws.com/860b369c4ceefd8b643713e955f487423b080e424cda8379ff0860c4bd92395f_small.jpg"
});
const segmindRequestOptions = {
method: "POST",
headers: segmindHeaders,
body: segmindRaw,
redirect: "follow"
};
// Change the URL for "Without Logo" option if needed
const segmindUrl = "https://api.segmind.com/workflows/67792f3899c1698a193130be-v2"; // Use 67d66d953fccc7a19c84cfa1-v1 for "Without Logo"
return fetch(segmindUrl, segmindRequestOptions)
.then((response) => response.json())
.then((segmindResult) => {
const pollUrl = segmindResult.poll_url;
// Step 3: Poll for the final image URL
const pollHeaders = new Headers();
pollHeaders.append("x-api-key", "");
const pollRequestOptions = {
method: "GET",
headers: pollHeaders,
redirect: "follow"
};
// Polling loop
const pollForResult = () => {
fetch(pollUrl, pollRequestOptions)
.then((pollResponse) => pollResponse.json())
.then((pollResult) => {
if (pollResult.status === "COMPLETED") {
const output = JSON.parse(pollResult.output);
const imageUrl = output[0].value.data;
console.log("Final Banner URL:", imageUrl);
// Display the banner
const imgElement = document.createElement("img");
imgElement.src = imageUrl;
document.body.appendChild(imgElement);
} else {
console.log("Still processing, status:", pollResult.status);
setTimeout(pollForResult, 5000); // Retry after 5 seconds
}
})
.catch((error) => console.error("Polling Error:", error));
};
pollForResult();
});
})
.catch((error) => console.error("Prompt Generation Error:", error));
```
### Error Handling
* **Prompt Generation Errors**:
* **Invalid API Key**: Returns "Unauthorized".
* **Missing Fields**: If `command_name`, `description`, or `category` is missing, an error may occur.
* **Segmind Submission Errors**:
* **401 Unauthorized**: If the Segmind API key is invalid.
* **400 Bad Request**: If `input_description` or `input_seed` is missing (or `input_logo` for the "With Logo" option).
* **Polling Errors**:
* **404 Not Found**: If the `request_id` is invalid.
* **503 Service Unavailable**: If Segmind is down.
#### Example Error Output (Segmind Submission)
```json theme={null}
{"error":"Invalid API key"}
```
### Best Practices
1. **Input Quality**: Provide a detailed `description` and relevant `category` for optimal banner prompts.
2. **Logo URL**: For the "With Logo" option, ensure the `input_logo` URL is accessible and points to a valid image.
3. **Polling Strategy**: Use a reasonable polling interval (e.g., 5-10 seconds) to avoid overloading the Segmind API.
4. **Error Handling**: Implement retry logic for polling to handle transient failures.
### Support
For assistance or to request API keys (for either the prompt generation or Segmind API), contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Provide details such as your use case and expected request volume.
# Segmind Logo generation
Source: https://docs.droplinked.com/api-reference/ai/segmind-logo-generation
Generate logos via Segmind's image generation pipeline, with model + style controls.
### Overview
The Segmind Logo Generation API generates a logo image by combining a prompt generation step with Segmind's image generation capabilities. It first uses the existing Logo Prompt Generator to create a text prompt from a shop description and category, then submits this prompt to Segmind's API to generate the logo. The process involves polling for the final image URL, making this tool ideal for businesses needing professional logos with a web3 focus.
### API Endpoint
* **URL**: This API involves multiple endpoints:
* Prompt Generation: `https://text-tad.droplinked.workers.dev/`
* Segmind Workflow Submission: `https://api.segmind.com/workflows/67792adb99c1698a193130bb-v5`
* Segmind Polling: `https://api.segmind.com/workflows/request/[request_id]`
* **Methods**:
* Prompt Generation: `POST`
* Workflow Submission: `POST`
* Polling: `GET`
* **Content-Type**: `application/json`
### Authentication
This API requires two API keys:
1. **For Prompt Generation** (via `text-tad.droplinked.workers.dev`):
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked).
2. **For Segmind API** (via `api.segmind.com`):
* **Header**: `x-api-key`
* **Value**: `` (your Segmind account API key).
Contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com) to obtain these API keys.
### Request Format
The process involves three steps:
#### Step 1: Generate Logo Prompt
This step uses the existing Logo Prompt Generator API to create a text prompt.
**Request Body**
| Field | Type | Required | Description |
| -------------- | ------ | -------- | ----------------------------------------------- |
| `command_name` | String | Yes | Must be set to `"logo_prompt"`. |
| `description` | String | Yes | A brief or detailed description of the company. |
| `category` | String | Yes | The company’s category (e.g., "technology"). |
**Example Request Body**
```json theme={null}
{
"command_name": "logo_prompt",
"description": "droplinked , we are a company offering web3 based and onchain solution to businesses and indivisuals .",
"category": "technology"
}
```
**Response**
A raw text prompt (e.g., `A futuristic and innovative logo for Droplinked, offering web3 and on-chain solutions to businesses and individuals.`).
***
#### Step 2: Submit to Segmind Workflow
Submit the generated prompt to Segmind's API to start logo generation.
**Request Body**
| Field | Type | Required | Description |
| ------------ | ------ | -------- | ---------------------------------------- |
| `input_logo` | String | Yes | The generated logo prompt from Step 1. |
| `input_seed` | String | Yes | A seed value for generation (e.g., "1"). |
**Example Request Body**
```json theme={null}
{
"input_logo": "A futuristic and innovative logo for Droplinked, offering web3 and on-chain solutions to businesses and individuals.",
"input_seed": "1"
}
```
**Response**
A JSON object with a polling URL and request ID.
**Example Response**
```json theme={null}
{
"message": "Your request with this id 68fe486045f6afa20e521094f20192ab currently in queue. Please be patient while we process it.",
"poll_url": "https://api.segmind.com/workflows/request/68fe486045f6afa20e521094f20192ab",
"request_id": "68fe486045f6afa20e521094f20192ab",
"status": "QUEUED"
}
```
***
#### Step 3: Poll for Final Image URL
Use the `poll_url` from Step 2 to check the status and retrieve the final image URL.
**Request**
* **Method**: `GET`
* **URL**: The `poll_url` from the Step 2 response (e.g., `https://api.segmind.com/workflows/request/68fe486045f6afa20e521094f20192ab`).
* **Headers**: Include the Segmind API key (`x-api-key`).
**Response**
A JSON object with the status and final image URL once completed.
**Example Response**
```json theme={null}
{
"output": "[{\"keyname\": \"output_logo\", \"value\": {\"data\": \"https://images.segmind.com/outputs/dbeae453-9a69-4622-83bf-8f4bd52eba49.png\", \"type\": \"image\"}}]",
"status": "COMPLETED"
}
```
### JavaScript Example
Below is an example of how to use the Segmind Logo Generation API in JavaScript with the `fetch` API:
```javascript theme={null}
// Step 1: Generate the logo prompt
const promptHeaders = new Headers();
promptHeaders.append("Content-Type", "application/json");
promptHeaders.append("api-key", ""); // Replace with your API key
const promptRaw = JSON.stringify({
"command_name": "logo_prompt",
"description": "droplinked , we are a company offering web3 based and onchain solution to businesses and indivisuals .",
"category": "technology"
});
const promptRequestOptions = {
method: "POST",
headers: promptHeaders,
body: promptRaw,
redirect: "follow"
};
fetch("https://text-tad.droplinked.workers.dev/", promptRequestOptions)
.then((response) => response.text())
.then((promptResult) => {
// Step 2: Submit the prompt to Segmind
const segmindHeaders = new Headers();
segmindHeaders.append("x-api-key", ""); // Replace with your Segmind API key
segmindHeaders.append("Content-Type", "application/json");
const segmindRaw = JSON.stringify({
"input_logo": promptResult,
"input_seed": "1"
});
const segmindRequestOptions = {
method: "POST",
headers: segmindHeaders,
body: segmindRaw,
redirect: "follow"
};
return fetch("https://api.segmind.com/workflows/67792adb99c1698a193130bb-v5", segmindRequestOptions)
.then((response) => response.json())
.then((segmindResult) => {
const pollUrl = segmindResult.poll_url;
// Step 3: Poll for the final image URL
const pollHeaders = new Headers();
pollHeaders.append("x-api-key", "");
const pollRequestOptions = {
method: "GET",
headers: pollHeaders,
redirect: "follow"
};
// Polling loop (simplified for example; implement proper interval logic)
const pollForResult = () => {
fetch(pollUrl, pollRequestOptions)
.then((pollResponse) => pollResponse.json())
.then((pollResult) => {
if (pollResult.status === "COMPLETED") {
const output = JSON.parse(pollResult.output);
const imageUrl = output[0].value.data;
console.log("Final Image URL:", imageUrl);
// Display the image
const imgElement = document.createElement("img");
imgElement.src = imageUrl;
document.body.appendChild(imgElement);
} else {
console.log("Still processing, status:", pollResult.status);
setTimeout(pollForResult, 5000); // Retry after 5 seconds
}
})
.catch((error) => console.error("Polling Error:", error));
};
pollForResult();
});
})
.catch((error) => console.error("Prompt Generation Error:", error));
```
### Error Handling
* **Prompt Generation Errors**:
* **Invalid API Key**: Returns "Unauthorized".
* **Missing Fields**: If `command_name`, `description`, or `category` is missing, an error may occur.
* **Segmind Submission Errors**:
* **401 Unauthorized**: If the Segmind API key is invalid.
* **400 Bad Request**: If `input_logo` or `input_seed` is missing.
* **Polling Errors**:
* **404 Not Found**: If the `request_id` is invalid.
* **503 Service Unavailable**: If Segmind is down.
#### Example Error Output (Segmind Submission)
```json theme={null}
{"error":"Invalid API key"}
```
### Best Practices
1. **Description Detail**: Provide a clear `description` and `category` for a relevant logo prompt.
2. **Polling Interval**: Implement a reasonable polling interval (e.g., 5-10 seconds) to avoid overloading the Segmind API.
3. **Error Handling**: Handle transient failures by retrying the polling step with exponential backoff.
4. **Image Usage**: Use the final image URL directly for display or download; ensure proper caching if needed.
### Support
For assistance or to request API keys (for either the prompt generation or Segmind API), contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Provide details such as your use case and expected request volume.
# Shop Descriptions Generator
Source: https://docs.droplinked.com/api-reference/ai/shop-descriptions-generator
Generate three concise, engaging shop descriptions from a business description, returned as strict JSON.
### API Endpoint
* **URL**: `https://text-tad.droplinked.workers.dev/`
* **Method**: `POST`
* **Content-Type**: `application/json`
### Authentication
An API key is required for access. Contact Droplinked support to obtain your API key. Include it in the request headers as follows:
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked)
### Request Format
The API accepts a JSON payload with the following fields:
#### Request Body
| Field | Type | Required | Description |
| -------------- | ------ | -------- | ------------------------------------------------------------ |
| `command_name` | String | Yes | Must be set to `"shop_descriptions"` for this functionality. |
| `description` | String | Yes | A brief or detailed description of the shop or business. |
#### Example Request Body
```json theme={null}
{
"command_name": "shop_descriptions",
"description": "A web3 platform offering decentralized cloud storage solutions for creators."
}
```
### Response Format
* **Content-Type**: `text/plain`
* **Response**: A strict JSON object containing exactly three shop descriptions, with no additional text or characters.
#### Response Structure
```json theme={null}
{
"shop_descriptions": ["Description 1", "Description 2", "Description 3"]
}
```
#### Example Response
```json theme={null}
{
"shop_descriptions": [
"A web3 platform delivering secure decentralized storage for creators.",
"Empower your creations with our onchain cloud storage solutions.",
"Decentralized storage redefined for the modern creator community."
]
}
```
### Guidelines for Shop Description Generation
The API generates output based on these rules:
1. **Shop Descriptions**:
* Exactly three concise and engaging descriptions derived from the `description`.
* Tailored to highlight key aspects of the business (e.g., web3, decentralized storage, creators).
2. **Output**:
* Strict JSON format with only the `"shop_descriptions"` key and a three-item array.
* No extra text, explanations, or additional fields.
### JavaScript Example
Below is an example of how to use the Shop Descriptions Generator API in JavaScript with the `fetch` API:
```javascript theme={null}
// Set up headers
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("api-key", ""); // Replace with your Droplinked API key
// Define the request payload
const raw = JSON.stringify({
"command_name": "shop_descriptions",
"description": "A web3 platform offering decentralized cloud storage solutions for creators."
});
// Configure request options
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
// Make the API call
fetch("https://text-tad.droplinked.workers.dev/", requestOptions)
.then((response) => response.text())
.then((result) => {
const data = JSON.parse(result);
console.log("Suggested Descriptions:", data.shop_descriptions);
})
.catch((error) => console.error(error));
```
### Error Handling
* **Invalid API Key**: Returns an error message (e.g., "Unauthorized").
* **Missing Fields**: If `command_name` or `description` is missing, the API may return an error or fail silently.
* **Network Issues**: Handle errors in the `.catch` block of the `fetch` call.
#### Example Error Output
```
Unauthorized - Invalid API Key
```
### Best Practices
1. **Description Specificity**: Include key business traits (e.g., products, technology, audience) for relevant descriptions.
2. **JSON Parsing**: Parse the response client-side to access the descriptions.
3. **Marketing Use**: Use the output for website copy, ads, or promotional materials.
4. **Testing**: Experiment with different descriptions to generate varied content.
### Support
For assistance or to request an API key, contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Provide details such as your use case and expected request volume.
# Shop Names Generator
Source: https://docs.droplinked.com/api-reference/ai/shop-names-generator
Suggest three shop names tailored to a business description, returned as strict JSON.
### API Endpoint
* **URL**: `https://text-tad.droplinked.workers.dev/`
* **Method**: `POST`
* **Content-Type**: `application/json`
### Authentication
An API key is required for access. Contact Droplinked support to obtain your API key. Include it in the request headers as follows:
* **Header**: `api-key`
* **Value**: `` (provided by Droplinked)
### Request Format
The API accepts a JSON payload with the following fields:
#### Request Body
| Field | Type | Required | Description |
| -------------- | ------ | -------- | -------------------------------------------------------- |
| `command_name` | String | Yes | Must be set to `"shop_names"` for this functionality. |
| `description` | String | Yes | A brief or detailed description of the shop or business. |
#### Example Request Body
```json theme={null}
{
"command_name": "shop_names",
"description": "A boutique selling handmade eco-friendly jewelry using blockchain for authenticity."
}
```
### Response Format
* **Content-Type**: `text/plain`
* **Response**: A strict JSON object containing exactly three shop name suggestions, with no additional text or characters.
#### Response Structure
```json theme={null}
{
"shop_names": ["Shop Name 1", "Shop Name 2", "Shop Name 3"]
}
```
#### Example Response
```json theme={null}
{
"shop_names": ["EcoChain Gems", "GreenLink Jewels", "Authentic EcoWear"]
}
```
### Guidelines for Shop Name Generation
The API generates output based on these rules:
1. **Shop Names**:
* Exactly three unique names reflecting the business concept from the `description`.
* Tailored to key themes (e.g., eco-friendliness, blockchain, jewelry in the example).
2. **Output**:
* Strict JSON format with only the `"shop_names"` key and a three-item array.
* No extra text, explanations, or additional fields.
### JavaScript Example
Below is an example of how to use the Shop Names Generator API in JavaScript with the `fetch` API:
```javascript theme={null}
// Set up headers
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("api-key", ""); // Replace with your Droplinked API key
// Define the request payload
const raw = JSON.stringify({
"command_name": "shop_names",
"description": "A boutique selling handmade eco-friendly jewelry using blockchain for authenticity."
});
// Configure request options
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
// Make the API call
fetch("https://text-tad.droplinked.workers.dev/", requestOptions)
.then((response) => response.text())
.then((result) => {
const data = JSON.parse(result);
console.log("Suggested Shop Names:", data.shop_names);
})
.catch((error) => console.error(error));
```
### Error Handling
* **Invalid API Key**: Returns an error message (e.g., "Unauthorized").
* **Missing Fields**: If `command_name` or `description` is missing, the API may return an error or fail silently.
* **Network Issues**: Handle errors in the `.catch` block of the `fetch` call.
#### Example Error Output
```
Unauthorized - Invalid API Key
```
### Best Practices
1. **Description Detail**: Include key aspects of the business (e.g., products, values, technology) for relevant names.
2. **JSON Parsing**: Parse the response client-side to access the shop names.
3. **Branding**: Use the suggestions as inspiration for official shop naming.
4. **Testing**: Experiment with different descriptions to explore naming options.
### Support
For assistance or to request an API key, contact Droplinked support at [support@droplinked.com](mailto:support@droplinked.com). Provide details such as your use case and expected request volume.
# Create a new blog post for shop
Source: https://docs.droplinked.com/api-reference/blog/create-a-new-blog-post-for-shop
https://apiv3.droplinked.com/swagger/json post /blogs
Route: BlogService.createBlog
# Delete a blog by ID
Source: https://docs.droplinked.com/api-reference/blog/delete-a-blog-by-id
https://apiv3.droplinked.com/swagger/json delete /blogs/{id}
Route: BlogService.deleteBlogById
# Get a blog by ID with all details
Source: https://docs.droplinked.com/api-reference/blog/get-a-blog-by-id-with-all-details
https://apiv3.droplinked.com/swagger/json get /blogs/{id}
Route: BlogService.getBlogById
# Get a public blog by slug with all details
Source: https://docs.droplinked.com/api-reference/blog/get-a-public-blog-by-slug-with-all-details
https://apiv3.droplinked.com/swagger/json get /blogs/public/{slug}
Route: BlogService.getPublicBlogBySlug
# Get all blogs of a shop with pagination and search
Source: https://docs.droplinked.com/api-reference/blog/get-all-blogs-of-a-shop-with-pagination-and-search
https://apiv3.droplinked.com/swagger/json get /blogs
Route: BlogService.getBlogsOfShop
# Get public blogs of a shop by name
Source: https://docs.droplinked.com/api-reference/blog/get-public-blogs-of-a-shop-by-name
https://apiv3.droplinked.com/swagger/json get /blogs/public/shops/{shopName}
Route: BlogService.getPublicBlogsByShopName
# Update a blog by ID with the provided data
Source: https://docs.droplinked.com/api-reference/blog/update-a-blog-by-id-with-the-provided-data
https://apiv3.droplinked.com/swagger/json put /blogs/{id}
Route: BlogService.updateBlogById
# Add product to cart
Source: https://docs.droplinked.com/api-reference/cart/add-product-to-cart
https://apiv3.droplinked.com/swagger/json post /v2/carts/{cartId}/products
Route: CartServiceV2.addProductToCart
# Apply coupon to cart
Source: https://docs.droplinked.com/api-reference/cart/apply-coupon-to-cart
https://apiv3.droplinked.com/swagger/json post /v2/carts/{cartId}/coupon
Route: CartServiceV2.applyCoupon
# Create a new cart
Source: https://docs.droplinked.com/api-reference/cart/create-a-new-cart
https://apiv3.droplinked.com/swagger/json post /v2/carts
Route: CartServiceV2.createCart
# Delete cart by ID
Source: https://docs.droplinked.com/api-reference/cart/delete-cart-by-id
https://apiv3.droplinked.com/swagger/json delete /v2/carts/{cartId}
Route: CartServiceV2.deleteCart
# Get available payment methods for cart
Source: https://docs.droplinked.com/api-reference/cart/get-available-payment-methods-for-cart
https://apiv3.droplinked.com/swagger/json get /v2/carts/{cartId}/payment-methods
Route: CartServiceV2.getCartPaymentMethods
# Get available shipping rates for cart
Source: https://docs.droplinked.com/api-reference/cart/get-available-shipping-rates-for-cart
https://apiv3.droplinked.com/swagger/json get /v2/carts/{cartId}/shipping
Route: CartServiceV2.getAvailableShippingRates
# Get cart by ID
Source: https://docs.droplinked.com/api-reference/cart/get-cart-by-id
https://apiv3.droplinked.com/swagger/json get /v2/carts/{cartId}
Route: CartServiceV2.getCart
# Remove coupon from cart
Source: https://docs.droplinked.com/api-reference/cart/remove-coupon-from-cart
https://apiv3.droplinked.com/swagger/json delete /v2/carts/{cartId}/coupon
Route: CartServiceV2.removeCoupon
# Remove product from cart
Source: https://docs.droplinked.com/api-reference/cart/remove-product-from-cart
https://apiv3.droplinked.com/swagger/json delete /v2/carts/{cartId}/products/{skuId}
Route: CartServiceV2.removeProductFromCart
# Select shipping rate for cart
Source: https://docs.droplinked.com/api-reference/cart/select-shipping-rate-for-cart
https://apiv3.droplinked.com/swagger/json post /v2/carts/{cartId}/shipping
Route: CartServiceV2.selectShippingRate
# Update cart customer information
Source: https://docs.droplinked.com/api-reference/cart/update-cart-customer-information
https://apiv3.droplinked.com/swagger/json patch /v2/carts/{cartId}/customer
Route: CartServiceV2.updateCartCustomer
# Update cart details
Source: https://docs.droplinked.com/api-reference/cart/update-cart-details
https://apiv3.droplinked.com/swagger/json patch /v2/carts/{cartId}/details
Route: CartServiceV2.updateCartDetails
# Update product quantity in cart
Source: https://docs.droplinked.com/api-reference/cart/update-product-quantity-in-cart
https://apiv3.droplinked.com/swagger/json patch /v2/carts/{cartId}/products/{skuId}
Route: CartServiceV2.updateProductQuantity
# Add product to collection (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/collection/add-product-to-collection-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json patch /collection-v2/{id}/add-product
Route: CollectionV2Service.addProductToCollection
# Create a new collection (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/collection/create-a-new-collection-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json post /collection-v2
Route: CollectionV2Service.create
# Delete collection by ID (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/collection/delete-collection-by-id-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json delete /collection-v2/{id}
Route: CollectionV2Service.remove
# Get all collections for the authenticated shop (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/collection/get-all-collections-for-the-authenticated-shop-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json get /collection-v2
Route: CollectionV2Service.findAll
# Get collection by ID (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/collection/get-collection-by-id-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json get /collection-v2/{id}
Route: CollectionV2Service.findOne
# Get collections by shop name (public)
Source: https://docs.droplinked.com/api-reference/collection/get-collections-by-shop-name-public
https://apiv3.droplinked.com/swagger/json get /collection-v2/by-shop-name
Route: CollectionV2Service.getCollectionByShopName
# Get collections for authenticated user (JWT required)
Source: https://docs.droplinked.com/api-reference/collection/get-collections-for-authenticated-user-jwt-required
https://apiv3.droplinked.com/swagger/json get /collection-v2/my-collections
Route: CollectionV2Service.getCollectionByAuth
# Get published collections by shop name (public)
Source: https://docs.droplinked.com/api-reference/collection/get-published-collections-by-shop-name-public
https://apiv3.droplinked.com/swagger/json get /collection-v2/published/{shopName}
Route: CollectionV2Service.getPublishedCollectionsByShopName
# Publish collection (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/collection/publish-collection-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json patch /collection-v2/{id}/publish
Route: CollectionV2Service.publish
# Reorder collections (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/collection/reorder-collections-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json patch /collection-v2/reorder
Route: CollectionV2Service.reorder
# Unpublish collection (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/collection/unpublish-collection-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json patch /collection-v2/{id}/unpublish
Route: CollectionV2Service.unpublish
# Update collection by ID (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/collection/update-collection-by-id-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json put /collection-v2/{id}
Route: CollectionV2Service.update
# Add an existing address to a customer
Source: https://docs.droplinked.com/api-reference/customer/add-an-existing-address-to-a-customer
https://apiv3.droplinked.com/swagger/json post /customer/{customerId}/addresses/add
Route: CustomerAddressV2Service.addAddressToCustomerFromDto
# Create a new address for a customer
Source: https://docs.droplinked.com/api-reference/customer/create-a-new-address-for-a-customer
https://apiv3.droplinked.com/swagger/json post /customer/{customerId}/addresses
Route: CustomerAddressV2Service.createCustomerAddressFromDto
# Create a new customer account
Source: https://docs.droplinked.com/api-reference/customer/create-a-new-customer-account
https://apiv3.droplinked.com/swagger/json post /customer/create
Route: CustomerV2Service.createCustomerFromDto
# Delete a customer address
Source: https://docs.droplinked.com/api-reference/customer/delete-a-customer-address
https://apiv3.droplinked.com/swagger/json delete /customer/{customerId}/addresses/{addressId}
Route: CustomerAddressV2Service.deleteCustomerAddressWithResponse
# Get all addresses for a customer
Source: https://docs.droplinked.com/api-reference/customer/get-all-addresses-for-a-customer
https://apiv3.droplinked.com/swagger/json get /customer/{customerId}/addresses
Route: CustomerAddressV2Service.getCustomerAddressesWithFormatting
# Get all customers for a shop (merchant only)
Source: https://docs.droplinked.com/api-reference/customer/get-all-customers-for-a-shop-merchant-only
https://apiv3.droplinked.com/swagger/json get /customer/shop
Route: CustomerV2Service.getCustomersByShop
# Get customer by email address
Source: https://docs.droplinked.com/api-reference/customer/get-customer-by-email-address
https://apiv3.droplinked.com/swagger/json get /customer/by-email
Route: CustomerV2Service.getCustomerByEmailFromDto
# Get customer by ID
Source: https://docs.droplinked.com/api-reference/customer/get-customer-by-id
https://apiv3.droplinked.com/swagger/json get /customer/{customerId}
Route: CustomerV2Service.getCustomerById
# Get customer by wallet address
Source: https://docs.droplinked.com/api-reference/customer/get-customer-by-wallet-address
https://apiv3.droplinked.com/swagger/json get /customer/by-wallet
Route: CustomerV2Service.getCustomerByWalletFromDto
# Get customer order IDs
Source: https://docs.droplinked.com/api-reference/customer/get-customer-order-ids
https://apiv3.droplinked.com/swagger/json get /customer/{customerId}/order-ids
Route: CustomerV2Service.getCustomerOrderIds
# Update a customer address
Source: https://docs.droplinked.com/api-reference/customer/update-a-customer-address
https://apiv3.droplinked.com/swagger/json put /customer/{customerId}/addresses/{addressId}
Route: CustomerAddressV2Service.updateCustomerAddressFromDto
# Apply gift card to cart (anonymous user) — DISABLED, see #1036
Source: https://docs.droplinked.com/api-reference/gift-card/apply-gift-card-to-cart-anonymous-user-—-disabled-see-#1036
https://apiv3.droplinked.com/swagger/json patch /giftcard/public/apply/{code}/{cartId}
Route: GiftCardService.applyGiftCard
# Apply gift card to cart (authenticated user) — DISABLED, see #1036
Source: https://docs.droplinked.com/api-reference/gift-card/apply-gift-card-to-cart-authenticated-user-—-disabled-see-#1036
https://apiv3.droplinked.com/swagger/json patch /giftcard/apply/{code}
Route: GiftCardService.applyGiftCard
# Create a new gift card
Source: https://docs.droplinked.com/api-reference/gift-card/create-a-new-gift-card
https://apiv3.droplinked.com/swagger/json post /giftcard/create
Route: GiftCardService.createGiftCard
# Export gift cards to Excel file
Source: https://docs.droplinked.com/api-reference/gift-card/export-gift-cards-to-excel-file
https://apiv3.droplinked.com/swagger/json get /giftcard/{giftCardId}/report/export/excel
Route: GiftCardService.giftCardsExcelReport
# Get all gift cards with pagination
Source: https://docs.droplinked.com/api-reference/gift-card/get-all-gift-cards-with-pagination
https://apiv3.droplinked.com/swagger/json get /giftcard
Route: GiftCardService.getAllGiftCards
# Remove gift card from cart (anonymous user)
Source: https://docs.droplinked.com/api-reference/gift-card/remove-gift-card-from-cart-anonymous-user
https://apiv3.droplinked.com/swagger/json patch /giftcard/public/remove/{cartId}
Route: GiftCardService.removeGiftCard
# Remove gift card from cart (authenticated user)
Source: https://docs.droplinked.com/api-reference/gift-card/remove-gift-card-from-cart-authenticated-user
https://apiv3.droplinked.com/swagger/json patch /giftcard/remove
Route: GiftCardService.removeGiftCard
# Update gift card expiration date
Source: https://docs.droplinked.com/api-reference/gift-card/update-gift-card-expiration-date
https://apiv3.droplinked.com/swagger/json patch /giftcard/expire/{id}
Route: GiftCardService.updateExpireDate
# API Reference
Source: https://docs.droplinked.com/api-reference/introduction
The Droplinked REST API — base URLs, auth, and conventions.
The Droplinked API is a REST API over JSON. The endpoint pages in this section are generated
from the live OpenAPI specification (`Droplinked APIs`, v1.2) and grouped by domain — shops,
products, carts, orders, collections, customers, subscriptions, AI tools, web3, and more.
## Base URL
```
https://apiv3.droplinked.com # production
https://apiv3dev.droplinked.com # development
```
## Authentication
* **Public** endpoints (`/public/…`) need no credentials.
* **Authenticated** endpoints use a Bearer JWT: `Authorization: Bearer `.
* **Integration** (server-to-server) calls use the `integration-api-key` header.
See [Authentication](/authentication) for how to obtain each.
```json theme={null}
"securitySchemes": {
"bearer": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" }
}
```
## Conventions
* Responses are wrapped as `{ statusCode, message, data }`; the payload is in `data`.
* List endpoints page with `?page=` and `?limit=`.
* Versioned resources use `v2`/`-v2` paths (e.g. `shops/v2`, `product-v2`, `order-v2`).
The interactive Swagger UI is available on development at
`https://apiv3dev.droplinked.com/api-doc`. The raw spec backing these pages lives at
`/api-doc-json`.
# Login merchant with email and password
Source: https://docs.droplinked.com/api-reference/merchant/login-merchant-with-email-and-password
https://apiv3.droplinked.com/swagger/json post /merchant/login
Route: MerchantAuthV2Service.loginMerchant
# Logout merchant and invalidate tokens
Source: https://docs.droplinked.com/api-reference/merchant/logout-merchant-and-invalidate-tokens
https://apiv3.droplinked.com/swagger/json post /merchant/logout
Route: MerchantAuthV2Service.logoutMerchant
# Refresh access token using refresh token
Source: https://docs.droplinked.com/api-reference/merchant/refresh-access-token-using-refresh-token
https://apiv3.droplinked.com/swagger/json post /merchant/refresh
Route: MerchantAuthV2Service.refreshToken
# Register a new merchant account
Source: https://docs.droplinked.com/api-reference/merchant/register-a-new-merchant-account
https://apiv3.droplinked.com/swagger/json post /merchant/register
Route: MerchantAuthV2Service.registerMerchant
# Request password reset for merchant account
Source: https://docs.droplinked.com/api-reference/merchant/request-password-reset-for-merchant-account
https://apiv3.droplinked.com/swagger/json post /merchant/forgot-password
Route: MerchantAuthV2Service.forgotPassword
# Reset merchant password with reset token
Source: https://docs.droplinked.com/api-reference/merchant/reset-merchant-password-with-reset-token
https://apiv3.droplinked.com/swagger/json post /merchant/reset-password
Route: MerchantAuthV2Service.resetPassword
# Verify password reset code
Source: https://docs.droplinked.com/api-reference/merchant/verify-password-reset-code
https://apiv3.droplinked.com/swagger/json post /merchant/verify-reset-code
Route: MerchantAuthV2Service.verifyResetCode
# OpenAPI Specification
Source: https://docs.droplinked.com/api-reference/openapi-spec
Machine-readable + human-readable API specs for droplinked. Use the rendered API Reference for browsing, or pull the raw JSON for code generation, Postman/Insomnia import, and agent tooling.
Three ways to consume the droplinked API spec:
* **This rendered reference** — browse all endpoints in the **API Reference** tab (auto-generated from the OpenAPI feed)
* **Interactive Swagger UI** — try requests live at [apiv3.droplinked.com/swagger/dev-docs](https://apiv3.droplinked.com/swagger/dev-docs)
* **Raw OpenAPI 3.0 JSON** — pull for client codegen, Postman import, agent SDK setup: [apiv3.droplinked.com/swagger/json](https://apiv3.droplinked.com/swagger/json)
## Live spec
Public, always-current. Re-generated on each backend deploy from the NestJS controller decorators. Used by this Mintlify reference + the MCP server + any agent tooling.
Rendered HTML Swagger UI for trying requests directly against prod with your API key. Same data as the JSON above.
## Code generation
```bash openapi-generator theme={null}
# Generate a TypeScript SDK from the live spec
npm install -g @openapitools/openapi-generator-cli
openapi-generator-cli generate \
-i https://apiv3.droplinked.com/swagger/json \
-g typescript-axios \
-o ./droplinked-sdk
```
```bash postman theme={null}
# Import directly into Postman
curl -sS https://apiv3.droplinked.com/swagger/json > droplinked.openapi.json
# Postman → File → Import → upload droplinked.openapi.json
```
```python python theme={null}
# Quick pythonic browse via openapi-spec-validator
pip install openapi-spec-validator requests
python -c "
import requests
spec = requests.get('https://apiv3.droplinked.com/swagger/json').json()
print(f'{len(spec[\"paths\"])} endpoints across {len(set(p.split(\"/\")[1] for p in spec[\"paths\"]))} resources')"
```
## Agent SDK setup
For Claude Code / Cursor / GitHub Copilot — add this docs site as an MCP server (see the floating contextual toolbar in the bottom-right of every page for one-click "Add to MCP" buttons). Or wire the OpenAPI JSON directly into your agent's tool definitions:
droplinked exposes its public APIs to agents via MCP — see the dedicated page for tools available + connection instructions.
## Spec stats
* OpenAPI version: **3.0.0**
* Total endpoints: **83+** (verified live)
* Auth: API key via `X-API-Key` header for public endpoints, JWT for merchant scope, customer-JWT for guest-checkout scope
* Versioning: per-module (v2 suffix where present, e.g., `/shops/v2`, `/customer-v2`, `/v2/carts`, `/v2/orders`)
* Updated: on every backend deploy (re-generated from NestJS @ApiTags decorators)
## Stability + change policy
Endpoint contracts are stable across patch + minor backend releases. Major version bumps (e.g., v2 → v3) appear under new module paths to keep current clients green. Subscribe to backend repo's release notes for breaking-change advance notice.
# Export order report as Excel file (JWT required)
Source: https://docs.droplinked.com/api-reference/order/export-order-report-as-excel-file-jwt-required
https://apiv3.droplinked.com/swagger/json get /v2/orders/export
Route: OrderServiceV2.exportOrderReport
# Get order by ID
Source: https://docs.droplinked.com/api-reference/order/get-order-by-id
https://apiv3.droplinked.com/swagger/json get /v2/orders/{orderId}
Route: OrderServiceV2.getOrderById
# Get order status summary (JWT required)
Source: https://docs.droplinked.com/api-reference/order/get-order-status-summary-jwt-required
https://apiv3.droplinked.com/swagger/json get /v2/orders/status
Route: OrderServiceV2.getOrdersStatus
# Get shop orders with pagination (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/order/get-shop-orders-with-pagination-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json get /v2/orders
Route: OrderServiceV2.getShopOrders
# Initialize order from cart
Source: https://docs.droplinked.com/api-reference/order/initialize-order-from-cart
https://apiv3.droplinked.com/swagger/json post /v2/orders
Route: OrderServiceV2.initOrder
# Create a new product (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/product/create-a-new-product-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json post /product-v2
Route: ProductV2Service.createProduct
# Delete product by ID (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/product/delete-product-by-id-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json delete /product-v2/{id}
Route: ProductV2Service.deleteProduct
# Duplicate product by ID (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/product/duplicate-product-by-id-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json post /product-v2/{id}/duplicate
Route: ProductV2Service.duplicateProduct
# Get all products for the authenticated shop (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/product/get-all-products-for-the-authenticated-shop-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json get /product-v2
Route: ProductV2Service.listProducts
# Get all products without pagination (JWT required)
Source: https://docs.droplinked.com/api-reference/product/get-all-products-without-pagination-jwt-required
https://apiv3.droplinked.com/swagger/json get /product-v2/shop/all
Route: ProductV2Service.listAllProducts
# Get available filters for a shop (public)
Source: https://docs.droplinked.com/api-reference/product/get-available-filters-for-a-shop-public
https://apiv3.droplinked.com/swagger/json get /product-v2/available/filters/{shopName}
Route: ProductV2Service.getAvailableFilters
# Get product by ID (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/product/get-product-by-id-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json get /product-v2/{id}
Route: ProductV2Service.getProduct
# Get product by ID (public)
Source: https://docs.droplinked.com/api-reference/product/get-product-by-id-public
https://apiv3.droplinked.com/swagger/json get /product-v2/public/{id}
Route: ProductV2Service.getProduct
# Get product by slug (public)
Source: https://docs.droplinked.com/api-reference/product/get-product-by-slug-public
https://apiv3.droplinked.com/swagger/json get /product-v2/public/by-slug/{slug}
Route: ProductV2Service.getProductBySlug
# Get products by shop name (public)
Source: https://docs.droplinked.com/api-reference/product/get-products-by-shop-name-public
https://apiv3.droplinked.com/swagger/json get /product-v2/public/shop/{shopName}
Route: ProductV2Service.getProductsByShopName
# Patch product by ID (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/product/patch-product-by-id-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json patch /product-v2/{id}
Route: ProductV2Service.patchProduct
# Reorder products (JWT required, PRODUCER role)
Source: https://docs.droplinked.com/api-reference/product/reorder-products-jwt-required-producer-role
https://apiv3.droplinked.com/swagger/json post /product-v2/reorder
Route: ProductV2Service.reorderProducts
# Upload external images (JWT required, PRODUCER/ADMIN role)
Source: https://docs.droplinked.com/api-reference/product/upload-external-images-jwt-required-produceradmin-role
https://apiv3.droplinked.com/swagger/json post /product-v2/upload/external/images
Route: ProductV2Service.uploadExternalImages
# Get abandoned cart details
Source: https://docs.droplinked.com/api-reference/public/abandoned-cart-details
Read full cart contents, customer info, and recovery timeline for a single abandoned cart scoped to the authenticated merchant's shop.
`GET /v2/merchant/shops/:shopId/abandoned-carts/:cartId` returns the **full report-ready envelope** for one abandoned cart: line items (with thumbnails, SKUs, qty × unit price, line total), customer contact, subtotal in the cart's currency, the recovery-event timeline, and the recovery token (until the cart is recovered).
This endpoint pairs with the [merchant list + stats](/api-reference/public/abandoned-carts-merchant) endpoints. The list returns row summaries; this endpoint returns the drawer-ready detail. Use this when a merchant clicks a row to see what was actually in the cart and how the recovery has progressed.
## When to use this
* The merchant dashboard renders a **cart-details drawer** when a merchant clicks a row in the abandoned-carts list
* A future details page wants the full cart payload + recovery timeline in one round-trip
* A pre-resend preview surface needs to confirm the contents before re-sending the recovery email
## Authentication
Merchant JWT (`PRODUCER` role) **plus** shop-ownership enforced server-side by the shared `MerchantShopScope` guard. The JWT-bound `shopId` claim must equal the `:shopId` path parameter. Cross-shop access returns `404` — never `403` — to avoid leaking the existence of `:cartId` to an enumeration attacker.
## Request
```
GET /v2/merchant/shops/:shopId/abandoned-carts/:cartId
```
| Param | Type | In | Notes |
| -------- | ------ | ---- | -------------------------------------------------------------------------------- |
| `shopId` | string | path | The shop's Mongo `_id`. PRODUCER's JWT-bound `shopId` MUST match. |
| `cartId` | string | path | The abandoned-cart row's Mongo `_id` (as returned by the list endpoint's `_id`). |
### Curl example
```bash theme={null}
curl -s "https://apiv3.droplinked.com/v2/merchant/shops/65f8.../abandoned-carts/65f8a1b2c3d4e5f6a7b8c9aa" \
-H "Authorization: Bearer $MERCHANT_JWT" | jq .
```
## Response (200)
```json theme={null}
{
"_id": "65f8a1b2c3d4e5f6a7b8c9aa",
"customerEmail": "buyer@example.com",
"customerPhone": null,
"items": [
{
"productId": "65f8a1b2c3d4e5f6a7b8caaa",
"productTitle": "Vintage Tee",
"sku": "TEE-001",
"quantity": 2,
"unitPrice": 24.50,
"lineTotal": 49.00,
"imageUrl": "https://cdn.droplinked.io/p/tee-vintage-thumb.jpg"
}
],
"subtotal": 49.00,
"currency": "USD",
"abandonedAt": "2026-06-10T18:23:00.000Z",
"lastSeenAt": "2026-06-10T18:42:00.000Z",
"recoveryEvents": [
{ "type": "EMAIL_SENT", "at": "2026-06-10T18:38:00.000Z", "channel": "email" },
{ "type": "EMAIL_OPENED", "at": "2026-06-10T19:01:00.000Z", "channel": "email" },
{ "type": "EMAIL_CLICKED", "at": "2026-06-10T19:01:30.000Z", "channel": "email" }
],
"recoveredAt": null,
"recoveryToken": "f3b8...e2c1"
}
```
### Field reference
| Field | Type | Notes |
| -------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `_id` | string | The abandoned-cart row id. |
| `customerEmail` | string \| null | `null` for guest checkouts. Render "Guest checkout" in the UI. |
| `customerPhone` | string \| null | Null on v1 rows — reserved for v1.1 SMS-channel onboarding. |
| `items[]` | array | One entry per line item snapshot at the time the cart was abandoned. May be empty for legacy rows. |
| `items[].productId` | string | Source product `_id`. |
| `items[].productTitle` | string | Display title (denormalized at snapshot time). |
| `items[].sku` | string \| null | Null on legacy v1 rows that pre-date SKU denormalization. |
| `items[].quantity` | int | Cart quantity at abandonment. |
| `items[].unitPrice` | number | **MAJOR units** of `currency` (e.g. dollars, not cents). |
| `items[].lineTotal` | number | `quantity × unitPrice`, pre-computed server-side. |
| `items[].imageUrl` | string \| null | Thumbnail. Null on legacy v1 rows that pre-date image-URL denormalization. |
| `subtotal` | number | Sum of `lineTotal`s in MAJOR units. |
| `currency` | string | ISO-4217 code (e.g. `USD`, `EUR`, `JPY`). |
| `abandonedAt` | ISO-8601 string | When the recovery cron flagged the cart. |
| `lastSeenAt` | ISO-8601 string \| null | Last update on the source cart-v2 cart (customer's last interaction before bouncing). |
| `recoveryEvents[]` | array | Recovery-attempt timeline, chronological. Empty for legacy rows. |
| `recoveryEvents[].type` | enum | `EMAIL_SENT` \| `EMAIL_OPENED` \| `EMAIL_CLICKED` \| `RECOVERED` |
| `recoveryEvents[].at` | ISO-8601 string | When the event occurred. |
| `recoveryEvents[].channel` | enum | `email` \| `sms` \| `storefront` |
| `recoveredAt` | ISO-8601 string \| null | When the cart was recovered. Null until the checkout-success chokepoint flips the row. |
| `recoveryToken` | string \| null | **Present ONLY when `recoveredAt === null`** — the backend redacts the token after recovery to keep the leak surface minimal. Build the storefront resume URL as `https://droplinked.io//cart?recover=`. |
### Money fields are MAJOR units
Unlike the [list endpoint](/api-reference/public/abandoned-carts-merchant) which serializes `cartTotalCents` (cents, integer minor units), the details endpoint returns `subtotal`, `unitPrice`, and `lineTotal` in **MAJOR units of `currency`**. The backend's projection layer converts once at the boundary using a zero-decimal-currency table (e.g. JPY/KRW have no fractional unit; USD/EUR have 2 decimals). The client can pass the values straight into `Intl.NumberFormat({ style: 'currency', currency })` without dividing by 100.
This contract diverges from the list endpoint intentionally — the list optimizes for the table cell where minor-unit integers avoid float drift across aggregates; the drawer endpoint pre-formats the merchant-friendly value so the UI doesn't need to know the currency-decimals table.
### Recovery token redaction
The backend includes `recoveryToken` in the response **only** while `recoveredAt === null`. After the checkout-success chokepoint flips the row, the token is operationally useless (the cart cannot be re-recovered) and exposing it widens the leak surface. The merchant dashboard hides the "copy recovery link" CTA whenever the field is null — defense-in-depth even though the BE already redacts.
### Recovery event channels
| Channel | Source |
| ------------ | ------------------------------------------------------------------------------------------------------------ |
| `email` | The recovery email rail (MailerSend prod / nodemailer dev). |
| `sms` | Reserved for v1.1 SMS-channel onboarding (no events emitted today). |
| `storefront` | The `POST /v2/abandoned-cart-recovery/recover` resume endpoint (customer clicked through to the storefront). |
## Error responses
| Status | When |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401` | Missing/invalid JWT. |
| `400` | Malformed `cartId` (not a valid Mongo ObjectId). |
| `404` | Cart not found **OR** cart belongs to a different shop. Response body: `{ "error": "abandoned_cart_not_found" }`. **Clients must NOT distinguish the two cases** — the unified posture is an IDOR-correct response (a `403` would leak the cart id's existence to an enumeration attacker). |
The list + stats endpoints documented on the [merchant page](/api-reference/public/abandoned-carts-merchant) are migrating from a `403` to a `404` posture on cross-shop access to match this endpoint (tracked in droplinked-backend PR #1980). Once that lands, all three endpoints will share the unified `404` + `abandoned_cart_not_found` body.
## Drawer-style flow
```typescript theme={null}
const BASE = "https://apiv3.droplinked.com";
const SHOP_ID = "65f8a1b2c3d4e5f6a7b8c9cc";
const SHOP_SLUG = "demo-shop"; // From the shop's public URL slug
const JWT = process.env.MERCHANT_JWT!;
interface RecoveryEvent {
type: "EMAIL_SENT" | "EMAIL_OPENED" | "EMAIL_CLICKED" | "RECOVERED";
at: string;
channel: "email" | "sms" | "storefront";
}
interface CartDetails {
_id: string;
customerEmail: string | null;
customerPhone: string | null;
items: Array<{
productId: string;
productTitle: string;
sku: string | null;
quantity: number;
unitPrice: number;
lineTotal: number;
imageUrl: string | null;
}>;
subtotal: number;
currency: string;
abandonedAt: string;
lastSeenAt: string | null;
recoveryEvents: RecoveryEvent[];
recoveredAt: string | null;
recoveryToken: string | null;
}
async function loadCartDrawer(cartId: string) {
const res = await fetch(
`${BASE}/v2/merchant/shops/${SHOP_ID}/abandoned-carts/${cartId}`,
{ headers: { Authorization: `Bearer ${JWT}` } },
);
if (res.status === 404) {
// Cart not found OR cross-shop — same friendly state, no leak.
return { kind: "not_found" as const };
}
const cart = (await res.json()) as CartDetails;
const subtotalLabel = new Intl.NumberFormat(undefined, {
style: "currency",
currency: cart.currency,
}).format(cart.subtotal);
const recoveryUrl = cart.recoveryToken
? `https://droplinked.io/${SHOP_SLUG}/cart?recover=${encodeURIComponent(cart.recoveryToken)}`
: null; // Hidden when recoveredAt !== null
return { kind: "loaded" as const, cart, subtotalLabel, recoveryUrl };
}
```
## Related
* [Abandoned carts (merchant)](/api-reference/public/abandoned-carts-merchant) — list + recovery-stats endpoints (the table this drawer is launched from)
* [Resume an abandoned cart (public)](/api-reference/public/abandoned-cart-recovery) — the storefront-side endpoint customers hit from the recovery email link
* [Embed the trust-fabric widget](/guides/integrations/embed-trust-fabric-widget) — the dashboard chrome the drawer launches inside
* [Order lifecycle](/guides/order-lifecycle) — the chokepoint that flips a row to `RECOVERED`
# Resume an abandoned cart
Source: https://docs.droplinked.com/api-reference/public/abandoned-cart-recovery
Restore a customer's cart from a recovery URL link sent in an automated email. Public, no auth — recovery token is the auth.
`POST /v2/abandoned-cart-recovery/recover` is the **public** endpoint a storefront calls
when a customer clicks the **recover-cart link** in an automated abandoned-cart email. The
endpoint takes the opaque `recoveryToken` from the URL, looks up the abandoned cart, and
returns the cart contents so the storefront can re-hydrate the checkout exactly where the
customer left off.
The endpoint is unauthenticated by design — the recovery token **is** the auth. Tokens are
single-purpose (cart-resume only), scoped to one cart, time-bound (7 days), and never carry
PII in the URL.
The cron that fires the recovery emails (which shops are eligible, how often, the email
template) is operator-controlled and is documented in the operator runbook for cart
abandonment — link will land once that runbook page ships.
## When to use
Call this when your storefront loads a URL that contains a `recover=` query string
parameter (the recovery email links land on `/cart?recover=`). On success, populate
the customer's cart state from the response. On failure, fall back to an empty cart and
optionally surface a "this link has expired" notice.
Recovery tokens are random 24-character `base64url`-encoded strings. They are unique,
indexed, and contain no PII (no email, no shop slug, no product names). The URL is safe to
log at the storefront edge, in CDN access logs, and in analytics.
## POST /v2/abandoned-cart-recovery/recover
### Authentication
None — the `recoveryToken` is the auth. Tokens are single-use semantically (resumes the
same cart) and rate-limited per IP (60 req/min).
### Request body
| Field | Type | Required | Description |
| --------------- | ------ | -------- | ------------------------------------------------------------ |
| `recoveryToken` | string | Yes | The opaque token from the recovery URL (24 chars, base64url) |
### Example request
```bash theme={null}
curl -X POST https://apiv3.droplinked.com/v2/abandoned-cart-recovery/recover \
-H 'content-type: application/json' \
-d '{ "recoveryToken": "kQ7bN3pXm2vR8sLwT4yC1zJh" }'
```
### Response — 200 OK, cart found
```json theme={null}
{
"found": true,
"cart": {
"cartId": "65f8a1b2c3d4e5f6a7b8c9aa",
"shopId": "65f8a1b2c3d4e5f6a7b8c9bb",
"shopSlug": "unstoppable",
"lineItems": [
{
"productId": "prod_xyz",
"skuId": "sku_xyz_red_m",
"quantity": 2,
"unitPriceCents": 2500
}
],
"cartTotalCents": 5000,
"currency": "USD",
"customerEmail": "buyer@example.com",
"abandonedAt": "2026-06-10T18:23:00.000Z"
}
}
```
| Field | Type | Description |
| --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `found` | `true` | Token resolved to a recoverable cart |
| `cart.cartId` | string | The cart's `_id` — pass to checkout-intent on resume |
| `cart.shopId` | string | The shop the cart belongs to |
| `cart.shopSlug` | string | The shop's storefront slug — useful for redirecting to the right `*.droplinked.io` host if the link was opened on a different surface |
| `cart.lineItems` | array | The cart's line items at abandonment time |
| `cart.cartTotalCents` | int | Pre-discount cart subtotal |
| `cart.currency` | string | Three-letter ISO 4217 |
| `cart.customerEmail` | string | The email address the recovery message was sent to — for pre-filling the contact form |
| `cart.abandonedAt` | ISO-8601 | When the cron flagged the cart as abandoned |
### Response — 404 Not Found, token not recognized or expired
```json theme={null}
{
"found": false,
"reason": "recovery_token_not_found_or_expired"
}
```
The endpoint returns a single failure reason (`recovery_token_not_found_or_expired`) so
that the storefront cannot distinguish "this token never existed" from "this token has
expired" from a timing or enumeration perspective. From the storefront's POV the UX is the
same in either case: surface a "this recovery link is no longer valid" notice and load an
empty cart.
### Error responses
| Status | When |
| ------ | -------------------------------------------------------------------------- |
| `400` | Malformed body (missing `recoveryToken`, wrong type, length other than 24) |
| `404` | Token not recognized or expired (see above) |
| `429` | Rate limit exceeded for caller IP |
## Recovery URL format
The cron writes recovery URLs in this exact format:
```
https://{shopSlug}.droplinked.io/cart?recover=
```
The storefront SPA at that route reads the `recover` query parameter, calls this endpoint,
and hydrates the cart from the response. If your custom storefront does not run on
`*.droplinked.io`, you can still call this endpoint — the response includes `cart.shopSlug`
so you can verify the link was intended for the surface that loaded it.
## Expiration
Recovery tokens expire **7 days** from cart abandonment. After expiry the token is
permanently invalid — there is no re-issue. The customer would need to be served a fresh
abandonment email (which can only be sent once per cart per the cron policy).
The 7-day window is operator-tunable per shop in the cart-abandonment cron config, but
defaults to 7 days for every shop.
## Privacy
Recovery tokens are random 24-character `base64url` strings. They are unique, server-side
indexed, and contain **no PII** — no email address, no shop slug, no product IDs, no order
ID. The URL can safely appear in browser history, CDN access logs, and email-client
preview-mode renderings without leaking customer state.
The cart record the token resolves to does contain the customer's email — but that record
is only accessible via the token, never via enumeration.
## Related
* [Checkout payment-intent resolver](/api-reference/public/checkout-intent-resolver) —
the next step after the customer resumes their cart and proceeds to checkout.
* [Merchants overview](/concepts/for-merchants) — how cart recovery fits into the
merchant's order lifecycle.
* Operator runbook — cart-abandonment cron config and email templates — **coming**.
# Abandoned carts (merchant)
Source: https://docs.droplinked.com/api-reference/public/abandoned-carts-merchant
List your shop's abandoned carts + view recovery rate stats. Merchant-scoped — requires PRODUCER role + shop ownership.
`GET /v2/merchant/shops/:shopId/abandoned-carts` and `GET /v2/merchant/shops/:shopId/abandoned-carts/stats` are the merchant-facing surface for the abandoned-cart recovery admin page. They let the shop owner view their own recovery funnel — the paginated list of abandoned carts and the top-of-page stat cards (active / recovered / recoveryRate).
These endpoints are gated by `RoleGuard([PRODUCER, SUPER_ADMIN])` PLUS an in-handler shop-ownership assertion: a PRODUCER's JWT-bound `shopId` claim must equal the `:shopId` path parameter. Cross-shop reads from a merchant token return `403`. SUPER\_ADMIN tokens carry no `shopId` claim and pass through for operator impersonation.
## Scope
These endpoints are the **merchant-scoped** counterpart to the SUPER\_ADMIN admin surface at `/admin/shops/:shopId/abandoned-carts`. The original admin route remains mounted at its original path with its original guard — this is strictly additive. The recovery cron, schema, and write-path are untouched.
| Surface | Path | Guard |
| ------------------------------ | ------------------------------------------------------------------------------------------- | -------------------------------------- |
| Public storefront resume | [`POST /v2/abandoned-cart-recovery/recover`](/api-reference/public/abandoned-cart-recovery) | None (recovery token is the auth) |
| Merchant dashboard (this page) | `GET /v2/merchant/shops/:shopId/abandoned-carts*` | `PRODUCER` (own shop) or `SUPER_ADMIN` |
| Operator admin | `GET /admin/shops/:shopId/abandoned-carts*` | `SUPER_ADMIN` only |
## GET — list abandoned carts
```
GET /v2/merchant/shops/:shopId/abandoned-carts
```
Returns the merchant's abandoned carts, sorted newest-`abandonedAt`-first, paginated.
### Request
| Param | Type | In | Notes |
| ---------- | ------ | --------------- | ------------------------------------------------------------------------------------------------ |
| `shopId` | string | path | The shop's Mongo `_id`. PRODUCER's JWT-bound `shopId` MUST match. |
| `status` | enum | query, optional | `ABANDONED` \| `EMAIL_QUEUED` \| `EMAIL_SENT` \| `RECOVERED` \| `EXPIRED`. Omitted = all states. |
| `page` | int | query, optional | 1-indexed page number. Default `1`. Junk inputs clamp to `1`. |
| `pageSize` | int | query, optional | Default `100`, max `500`. Junk inputs clamp to `1`. |
### Curl example
```bash theme={null}
curl -s "https://apiv3.droplinked.com/v2/merchant/shops/65f8.../abandoned-carts?status=EMAIL_SENT&page=1&pageSize=20" \
-H "Authorization: Bearer $MERCHANT_JWT" | jq .
```
### Response (200)
```json theme={null}
{
"rows": [
{
"_id": "65f8a1b2c3d4e5f6a7b8c9aa",
"cartId": "65f8a1b2c3d4e5f6a7b8c9bb",
"shopId": "65f8a1b2c3d4e5f6a7b8c9cc",
"customerEmail": "buyer@example.com",
"customerName": "Jamie Doe",
"cartTotalCents": 5000,
"currency": "USD",
"status": "EMAIL_SENT",
"abandonedAt": "2026-06-10T18:23:00.000Z",
"emailSentAt": "2026-06-10T18:38:00.000Z",
"expiresAt": "2026-06-17T18:23:00.000Z",
"createdAt": "2026-06-10T18:23:00.000Z"
}
],
"total": 47,
"page": 1,
"pageSize": 20
}
```
### Lifecycle states
| `status` | Meaning |
| -------------- | --------------------------------------------------------------------------------------- |
| `ABANDONED` | Threshold crossed (cart with items + no checkout for N hours); awaiting next cron sweep |
| `EMAIL_QUEUED` | Cron selected this row for sending |
| `EMAIL_SENT` | Email rail accepted the send (recovery URL is live) |
| `RECOVERED` | Customer returned + checked out via the checkout-success chokepoint |
| `EXPIRED` | Past `expiresAt` (7 days from `abandonedAt`) without recovery |
### Error responses
| Status | When |
| ------ | --------------------------------------------------------------------------------------------------------- |
| `401` | Missing/invalid JWT |
| `403` | PRODUCER's `shopId` claim does not match the path `:shopId`, or token lacks `PRODUCER`/`SUPER_ADMIN` role |
## GET — recovery stats
```
GET /v2/merchant/shops/:shopId/abandoned-carts/stats
```
Aggregate counts for the top-of-page stat cards. Client-side aggregation over the paginated list would produce a wrong `recoveryRate` past page 1 — this endpoint computes the totals server-side over the full collection.
### Curl example
```bash theme={null}
curl -s "https://apiv3.droplinked.com/v2/merchant/shops/65f8.../abandoned-carts/stats" \
-H "Authorization: Bearer $MERCHANT_JWT" | jq .
```
### Response (200)
```json theme={null}
{
"activeCount": 12,
"recoveredCount": 4,
"recoveryRate": 0.25
}
```
### Field reference
| Field | Type | Notes |
| ---------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `activeCount` | int | Rows in `ABANDONED` \| `EMAIL_QUEUED` \| `EMAIL_SENT` — carts still in the recovery funnel. |
| `recoveredCount` | int | Rows in `RECOVERED` — the chokepoint flip happens at checkout-success (NOT email click-through), so this number directly tracks **converted** recoveries. |
| `recoveryRate` | float | `recoveredCount / (activeCount + recoveredCount)`, in `[0, 1]`. Zero-denominator edge case returns `0` (NOT `NaN` / `null`) — a brand-new shop with no abandoned-cart history would otherwise produce `0/0`. The FE renders `0` as `—` to avoid surfacing a misleading "0% recovery rate" before any data exists. |
`recoveryRate` is **checkout-success semantic**, not click-through. A customer who clicks the recovery email but bounces off checkout does NOT count as recovered. The chokepoint is `service.markRecovered` invoked from the checkout-success path — see the [`POST /v2/abandoned-cart-recovery/recover`](/api-reference/public/abandoned-cart-recovery) endpoint that resumes the cart upstream of that flip.
Both `countDocuments` calls run in parallel and hit the existing `{shopId, status, abandonedAt}` compound index — covered counts, no collection scan.
### Error responses
Same gating + 401/403 semantics as the list endpoint above.
## Typical merchant dashboard flow
```typescript theme={null}
const BASE = "https://apiv3.droplinked.com";
const SHOP_ID = "65f8a1b2c3d4e5f6a7b8c9cc";
const JWT = process.env.MERCHANT_JWT!;
interface Stats {
activeCount: number;
recoveredCount: number;
recoveryRate: number;
}
interface CartRow {
_id: string;
cartId: string;
customerEmail: string;
customerName: string | null;
cartTotalCents: number;
currency: string;
status: "ABANDONED" | "EMAIL_QUEUED" | "EMAIL_SENT" | "RECOVERED" | "EXPIRED";
abandonedAt: string;
}
interface ListResponse {
rows: CartRow[];
total: number;
page: number;
pageSize: number;
}
async function loadDashboard(page = 1) {
const headers = { Authorization: `Bearer ${JWT}` };
const [stats, list] = await Promise.all([
fetch(`${BASE}/v2/merchant/shops/${SHOP_ID}/abandoned-carts/stats`, { headers })
.then((r) => r.json() as Promise),
fetch(
`${BASE}/v2/merchant/shops/${SHOP_ID}/abandoned-carts?page=${page}&pageSize=20`,
{ headers },
).then((r) => r.json() as Promise),
]);
// Render stat cards: "—" when recoveryRate === 0 + no carts at all
const rateLabel =
stats.activeCount + stats.recoveredCount === 0
? "—"
: `${(stats.recoveryRate * 100).toFixed(1)}%`;
return { stats, list, rateLabel };
}
```
## Related
* [Get abandoned cart details](/api-reference/public/abandoned-cart-details) — full cart contents + recovery timeline for one row (drawer/detail surface)
* [Resume an abandoned cart (public)](/api-reference/public/abandoned-cart-recovery) — the storefront-side endpoint customers hit from the recovery email link
* [Merchants overview](/concepts/for-merchants) — where cart recovery fits in the merchant order lifecycle
* [Order lifecycle](/guides/order-lifecycle) — the chokepoint that flips a row to `RECOVERED`
# Brand attestation request
Source: https://docs.droplinked.com/api-reference/public/brand-attestation-request
Submit a brand attestation request + poll its lifecycle status. Backed by an operator-curated review + EAS Schema A mint.
`POST /v2/attestations/brand/:shopSlug/request` and `GET /v2/attestations/brand/:shopSlug/request-status` are the merchant-facing CTA + polling endpoints behind the trust-fabric dashboard widget's **Request brand attestation** modal. Together they queue an operator-reviewed request and let the storefront flip its UI between the four lifecycle states without leaving the dashboard.
Both endpoints are **public** — no JWT, no IP-allowlist. The `:shopSlug` is the URL-safe brand slug (same value the on-chain `BrandAttestation.brandSlug` carries — NOT the human-display name).
## Lifecycle
A request walks four states. The on-chain Schema A mint is gated behind operator review — clicking the CTA does NOT directly mint:
| State | Meaning |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PENDING` | Merchant has clicked the CTA. Awaiting SUPER\_ADMIN review. |
| `APPROVED` | Operator has approved the queue row. The mint orchestrator will attempt a Schema A on-chain mint. On mint failure the row STAYS `APPROVED` (`mintError` + `mintAttempts` persist for retry); it is never auto-flipped back. |
| `MINTED` | Schema A attestation is on-chain. `attestationUid` + `mintedAt` are populated and the widget can quote the EAS reference without a second round-trip. |
| `REJECTED` | Operator declined the row with a reason. The merchant may re-submit a fresh `PENDING` row (the partial-unique index on `(shopSlug, status: PENDING)` does NOT block re-submits after a terminal state). |
The status endpoint also reports a synthetic `NONE` state when no request row exists for the slug (used by the widget to render the initial CTA).
## POST — submit a request
```
POST /v2/attestations/brand/:shopSlug/request
```
Idempotent on `(shopSlug, status: PENDING)`. Re-submitting while a `PENDING` row exists returns the same `requestId` instead of creating a duplicate — the widget always renders a success toast and never has to handle a "duplicate" 4xx.
### Request
| Param | Type | In | Notes |
| ---------- | ------ | -------------- | ----------------------------------------------------------------------------- |
| `shopSlug` | string | path | Kebab-case brand slug — same value as `BrandAttestation.brandSlug` |
| `notes` | string | body, optional | Free-text merchant note. Max 2048 chars. Surfaces on the operator queue only. |
### Curl example
```bash theme={null}
curl -X POST https://apiv3.droplinked.com/v2/attestations/brand/unstoppable/request \
-H 'content-type: application/json' \
-d '{ "notes": "Need this for the Q3 partner pitch on 2026-07-15" }'
```
### Response (200)
```json theme={null}
{
"requestId": "65f8a1b2c3d4e5f6a7b8c9aa",
"status": "PENDING",
"message": "Your request is in the operator review queue"
}
```
The response is intentionally minimal — operator-only fields (`decidedBy`, `decisionReason`, `merchantId`, internal `notes`) never cross the public surface. `status` is hard-cast to `PENDING` on this endpoint: an `APPROVED` / `MINTED` / `REJECTED` row can never be returned by the idempotency check (only `PENDING` matches the dedupe index).
### Error responses
| Status | When |
| ------ | ------------------------------------------------------- |
| `400` | `shopSlug` invalid or `notes` exceeds 2048 chars |
| `404` | No shop found for the slug (resolved via `ShopService`) |
## GET — poll the status
```
GET /v2/attestations/brand/:shopSlug/request-status
```
The widget polls this to walk through `NONE → PENDING → APPROVED → MINTED` (or `→ REJECTED`) without leaving the merchant dashboard. Resolution rule: any `PENDING` row wins (there is at most one by index); otherwise the newest terminal row (`createdAt` desc) is returned.
### Curl example
```bash theme={null}
curl -s https://apiv3.droplinked.com/v2/attestations/brand/unstoppable/request-status | jq .
```
### Response (200, history exists)
```json theme={null}
{
"status": "MINTED",
"request": {
"requestId": "65f8a1b2c3d4e5f6a7b8c9aa",
"shopSlug": "unstoppable",
"status": "MINTED",
"attestationUid": "0x9c4f7a3e8b1d2c6f5a0b8e9d1c2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f",
"mintedAt": "2026-06-13T07:42:11Z",
"createdAt": "2026-06-12T18:00:00Z"
}
}
```
When `status === "MINTED"` the `attestationUid` resolves on EAS Base via the standard easscan link scheme — e.g. `https://base.easscan.org/attestation/view/0x9c4f7a3e…` — so the widget can link the merchant directly to the on-chain record without a second round-trip.
### Response (200, no history)
```json theme={null}
{
"status": "NONE",
"request": null
}
```
The status endpoint **always** returns 200 — even when no request exists. The synthetic `NONE` discriminator lets the widget render the initial **Request brand attestation** CTA without 404-handling. Storefronts should treat `request === null` as the "Not requested" UI state.
### Field reference
| Field | Type | Notes |
| ------------------------ | ---------------- | ----------------------------------------------------------------------------------------------------- |
| `status` | enum | `NONE` \| `PENDING` \| `APPROVED` \| `MINTED` \| `REJECTED` |
| `request.requestId` | string | Mongo `_id` of the latest queue row |
| `request.shopSlug` | string | Normalised lowercase slug |
| `request.status` | enum | `PENDING` \| `APPROVED` \| `MINTED` \| `REJECTED` — never `NONE` (that state implies `request: null`) |
| `request.attestationUid` | string \| null | 0x-prefixed EAS attestation UID. Only populated when `status === "MINTED"` |
| `request.mintedAt` | ISO 8601 \| null | When the Schema A mint succeeded on-chain. Only populated when `status === "MINTED"` |
| `request.createdAt` | ISO 8601 \| null | When the merchant clicked the CTA |
Operator-private fields (`decidedBy`, `decisionReason`, `mintError`, `mintAttempts`, `merchantId`, free-text `notes`) are SCRUBBED from the public projection — they only surface on the SUPER\_ADMIN admin route.
## TypeScript flow example
A merchant dashboard widget driving the four-state UI:
```typescript theme={null}
type BrandAttestationStatus = "NONE" | "PENDING" | "APPROVED" | "MINTED" | "REJECTED";
interface StatusResponse {
status: BrandAttestationStatus;
request: {
requestId: string;
shopSlug: string;
status: Exclude;
attestationUid: string | null;
mintedAt: string | null;
createdAt: string | null;
} | null;
}
const BASE = "https://apiv3.droplinked.com";
async function fetchStatus(shopSlug: string): Promise {
const res = await fetch(`${BASE}/v2/attestations/brand/${shopSlug}/request-status`);
return res.json();
}
async function submitRequest(shopSlug: string, notes?: string) {
const res = await fetch(`${BASE}/v2/attestations/brand/${shopSlug}/request`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ notes: notes ?? null }),
});
return res.json();
}
// Initial render — pick the UI branch off the status discriminator
const s = await fetchStatus("unstoppable");
switch (s.status) {
case "NONE":
// Render the "Request brand attestation" CTA
break;
case "PENDING":
// Render "Pending operator review — we'll email you when it ships"
break;
case "APPROVED":
// Render "Approved — on-chain mint in progress"
break;
case "MINTED":
// Render the easscan link from request.attestationUid
break;
case "REJECTED":
// Render rejection notice + offer fresh re-submit
break;
}
```
## Related
* [Trust Fabric overview](/concepts/trust-fabric) — where Schema A brand attestations fit in the 4-axis architecture
* [Lender Registry Lookup](/api-reference/public/lender-registry) — the sibling registry pattern this module mirrors
* [Methodology Registry Lookup](/api-reference/public/methodology-registry) — Schema B verifier flow
* [Merchants overview](/concepts/for-merchants) — how the trust-fabric widget surfaces on the merchant dashboard
# Checkout Payment-Intent Resolver
Source: https://docs.droplinked.com/api-reference/public/checkout-intent-resolver
Public endpoint the checkout SPA calls to discover which PSPs to render for a shop — no JWT required.
`GET /checkout/:shopUrl/payment-intent-resolver` is the **public** endpoint the
`checkout-spa` calls on page load to determine which payment methods to render. It does
**not** require a JWT — `shopUrl` is the only input, and the response is safe to expose
publicly because it contains only payment-method *capabilities*, never customer state or
secrets.
This endpoint is public — **no JWT, no IpAllowlistGuard, no GeoBlockGuard**. It is rate-limited
per IP via the standard public-endpoint policy (60 req/min/IP). Cached at the CDN for 60s
keyed on `shopUrl`.
## When to use
Call this once at checkout page load to know which payment renderer to mount. The response
tells the FE:
* Which **card** processor to use (Stripe / Telr / Paymob / Bonum / null) + the right
`publishableKey`
* Whether to show **PayPal**
* Whether to show **crypto** (Coinbase Commerce)
* Whether to show **wallet** (Apple Pay / Google Pay tokens — routed through Bonum or Stripe
depending on the merchant config)
* The merchant's checkout `currency`
* Resolver `meta` — the source-of-truth chain the resolver walked (useful for debugging
"why am I seeing Stripe instead of Telr?")
## Source-of-truth resolution chain
The resolver walks the following sources in order, returning the first non-null match per
payment method:
The merchant's explicit per-shop payment-method config. If a method is **explicitly
disabled** here, it short-circuits — later sources cannot re-enable it.
If the merchant belongs to a partnership (e.g. `shopsadiq-telr-gcc`), the preset's
default card processor is applied. Currently in active rollout — once #1575 fully lands
per-merchant config, this step becomes the canonical source for partnership merchants.
The merchant's `kybCohort` (one of 8 enum values) drives the regional default — e.g.
GCC cohorts default to Telr, Mongolia cohorts default to Bonum, US cohorts default to
Stripe.
If the candidate PSP's circuit breaker is `open`, the resolver falls through to the
next viable option. The skipped PSP is recorded in `meta.skippedByBreaker[]`.
Final step — fetches the right publishable key from the per-PSP config (per-merchant
override if present, else env default).
The full walk is summarized in the `meta` field of the response, so a FE engineer can
inspect a single response and understand exactly why a given renderer was chosen.
## GET /checkout/:shopUrl/payment-intent-resolver
### Authentication
None — public endpoint. Rate-limited per IP (60 req/min).
### Path parameters
| Param | Type | Required | Description |
| --------- | ------ | -------- | ------------------------------------------------------------------------- |
| `shopUrl` | string | Yes | The shop's subdomain (e.g. `unstoppable` for `unstoppable.droplinked.io`) |
### Response — 200 OK
```json theme={null}
{
"card": {
"provider": "telr",
"label": "Card",
"accountId": "telr-shopsadiq-master",
"renderer": "telr-hosted",
"publishableKey": null
},
"paypal": {
"enabled": true,
"clientId": "AYxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"currency": "AED",
"intent": "capture"
},
"crypto": {
"enabled": false,
"provider": null
},
"wallet": {
"applePay": true,
"googlePay": true,
"tokenProvider": "telr"
},
"currency": "AED",
"meta": {
"shopId": "65f8a1b2c3d4e5f6a7b8c9bb",
"merchantId": "65f8a1b2c3d4e5f6a7b8c9aa",
"kybCohort": "A-MoR-Shopsadiq-via-Telr",
"partnership": "shopsadiq-telr-gcc",
"resolvedAt": "2026-06-04T12:00:00.000Z",
"chain": [
{ "step": "shopPaymentMethodsV2", "result": "no-explicit-override" },
{ "step": "partnership-preset", "result": "telr-via-shopsadiq" },
{ "step": "kyb-cohort", "result": "skipped (resolved earlier)" },
{ "step": "psp-breaker", "result": "telr-closed" },
{ "step": "publishable-key", "result": "telr-hosted-no-publishable-key" }
],
"skippedByBreaker": []
}
}
```
### Field reference
#### `card` — primary card renderer (or `null` if no card processor is wired)
| Field | Type | Description |
| ---------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| `provider` | enum (`stripe` \| `telr` \| `paymob` \| `bonum`) | Which PSP processes the card payment |
| `label` | string | Human-readable label for the renderer ("Card", "Credit/Debit Card") |
| `accountId` | string | The PSP-side account / terminal / merchant ID — informational only |
| `renderer` | enum (`stripe-elements` \| `telr-hosted` \| `paymob-iframe` \| `bonum-hosted`) | Which renderer component the FE should mount |
| `publishableKey` | string \| null | The PSP's client-side publishable key. `null` when the renderer is hosted-only (Telr, Bonum) |
#### `paypal`
| Field | Type | Description |
| ---------- | ------------------------------- | ---------------------------------------------- |
| `enabled` | boolean | Whether to show the PayPal button |
| `clientId` | string \| null | PayPal client ID for the JS SDK |
| `currency` | string | Three-letter ISO currency for the PayPal order |
| `intent` | enum (`capture` \| `authorize`) | PayPal intent mode |
#### `crypto`
| Field | Type | Description |
| ---------- | ------------------------- | ---------------------------------------------------- |
| `enabled` | boolean | Whether to show the crypto-pay option |
| `provider` | enum (`coinbase`) \| null | Crypto processor; only `coinbase` is currently wired |
#### `wallet`
| Field | Type | Description |
| --------------- | ------------------------------------ | ----------------------------------- |
| `applePay` | boolean | Whether to render Apple Pay |
| `googlePay` | boolean | Whether to render Google Pay |
| `tokenProvider` | enum (`stripe` \| `telr` \| `bonum`) | Which PSP consumes the wallet token |
#### `currency`
The merchant's checkout currency. Format three-letter ISO 4217 (`USD`, `AED`, `MNT`, `EUR`).
#### `meta`
Informational only — not consumed by the renderer. Captures `kybCohort`, `partnership`,
the full resolution `chain`, and any PSPs skipped because their breaker was open.
### Response — 200 OK with no card processor
```json theme={null}
{
"card": null,
"paypal": { "enabled": true, "clientId": "...", "currency": "USD", "intent": "capture" },
"crypto": { "enabled": false, "provider": null },
"wallet": { "applePay": false, "googlePay": false, "tokenProvider": null },
"currency": "USD",
"meta": { "...": "..." }
}
```
Returned when the merchant has no card processor wired (e.g. PayPal-only merchant, or
every candidate PSP has an open breaker).
### Error responses
| Status | When |
| ------ | -------------------------------------------- |
| `404` | `shopUrl` does not resolve to an active shop |
| `410` | Shop exists but is suspended / archived |
| `429` | Rate limit exceeded for caller IP |
### Example
```bash theme={null}
curl https://apiv3.droplinked.com/checkout/unstoppable/payment-intent-resolver
```
### JavaScript example
```javascript theme={null}
const res = await fetch(
`https://apiv3.droplinked.com/checkout/${shopUrl}/payment-intent-resolver`
);
const intent = await res.json();
if (intent.card) {
mountCardRenderer({
renderer: intent.card.renderer,
publishableKey: intent.card.publishableKey,
});
}
if (intent.paypal.enabled) {
mountPayPalButton({
clientId: intent.paypal.clientId,
currency: intent.paypal.currency,
intent: intent.paypal.intent,
});
}
if (intent.wallet.applePay || intent.wallet.googlePay) {
mountWallet({ tokenProvider: intent.wallet.tokenProvider });
}
```
## Debugging
When the FE renders the wrong processor (e.g. Stripe when you expected Telr), inspect the
`meta.chain` field — it explains exactly which step resolved the card processor. The most
common reasons for an unexpected provider are:
| Symptom | Likely cause |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Stripe rendered for a GCC merchant | `shopPaymentMethodsV2` has an explicit Stripe override; remove it |
| `card: null` for a merchant that has Telr wired | Telr breaker is `open` — check [PSP health](/api-reference/admin/psp-health) |
| `card.publishableKey: null` but renderer is `stripe-elements` | The per-merchant Stripe config is missing the publishable key — re-write via aggregate provisioner |
## Related
* [Bonum admin](/api-reference/admin/bonum) — per-merchant Bonum config (affects `wallet.tokenProvider`).
* [Telr admin](/api-reference/admin/telr) — Telr reconcile (does not affect resolver output directly, but breaker state does).
* [PSP health](/api-reference/admin/psp-health) — breaker state for every PSP the resolver consults.
* [Aggregate merchant provisioner](/api-reference/admin/aggregate-provisioner) — partnership-preset writes that drive resolver defaults.
# Validate a discount code
Source: https://docs.droplinked.com/api-reference/public/discounts-validate
Validate a merchant-issued coupon code against a cart shape. Returns whether the code applies + the discount amount in cents. Public, no auth.
`POST /v2/discounts/validate` is the **public** endpoint a checkout SPA (or a
partner-built custom checkout) calls to pre-validate a customer-entered coupon code against
the cart they are about to submit. It returns whether the code is eligible plus the
discount amount the customer would see at redemption time.
It is a **precondition** for redemption — the actual write (incrementing the code's usage
counter and applying the discount to the order) happens server-side at the
[checkout-intent resolver](/api-reference/public/checkout-intent-resolver) chokepoint when
the FE submits the cart with the validated `discountCode` field set.
Merchants create and configure their discount codes in the shop-builder admin (CRUD UI for
expiry / min-purchase / per-customer caps / scope). The shop-builder admin discount-creation
guide is **coming** — link will land once that page ships.
## When to use
Call this once when a customer enters a code in the checkout UI, before enabling the
"Apply" CTA or before posting the cart to checkout-intent. The endpoint never mutates
state — calling it repeatedly is safe and rate-limited per IP.
Partner integrators building a custom storefront on top of droplinked's catalog and
checkout backbone should call this endpoint immediately before
`POST /v2/checkout/intent` so the customer sees the post-discount total in the cart
summary that matches what redemption will actually charge.
This endpoint **does not redeem**. It does not decrement remaining uses, does not mark a
code as consumed, and does not bind the code to a customer. Redemption (incrementing usage
count, applying the line-item discount, recording the redemption event) happens at
`POST /v2/checkout/intent` when the `discountCode` field is provided.
## POST /v2/discounts/validate
### Authentication
None — public endpoint. Rate-limited per IP (60 req/min).
### Request body
| Field | Type | Required | Description |
| ---------------------------- | ------ | -------- | ----------------------------------------------------------------------------- |
| `shopId` | string | Yes | The shop the code belongs to (`Shop._id`) |
| `code` | string | Yes | The coupon code as the customer typed it. Case-insensitive on the server side |
| `cartTotalCents` | int | Yes | Pre-discount cart subtotal in the shop's currency, in cents |
| `lineItems` | array | Yes | Cart line items — used to evaluate scope/product-restricted codes |
| `lineItems[].productId` | string | Yes | `Product._id` |
| `lineItems[].quantity` | int | Yes | Units of this product in the cart |
| `lineItems[].unitPriceCents` | int | Yes | Per-unit price in cents, pre-discount |
### Example request
```bash theme={null}
curl -X POST https://apiv3.droplinked.com/v2/discounts/validate \
-H 'content-type: application/json' \
-d '{
"shopId": "abc123",
"code": "SAVE20",
"cartTotalCents": 5000,
"lineItems": [
{ "productId": "prod_xyz", "quantity": 1, "unitPriceCents": 5000 }
]
}'
```
### Response — 200 OK, code valid
```json theme={null}
{
"valid": true,
"discount": {
"code": "SAVE20",
"discountType": "percentage",
"discountValue": 20,
"discountAmountCents": 1000,
"currency": "USD"
},
"discountAmountCents": 1000
}
```
| Field | Type | Description |
| ------------------------------ | ------------------------------ | ---------------------------------------------------------------------------------------- |
| `valid` | `true` | Code is eligible against this cart shape |
| `discount.code` | string | Echoed code as stored (canonical case) |
| `discount.discountType` | enum (`percentage` \| `fixed`) | How the code reduces the cart |
| `discount.discountValue` | int | The percent (for `percentage`) or fixed-cents value (for `fixed`) configured on the code |
| `discount.discountAmountCents` | int | Computed discount in cents for this exact cart |
| `discount.currency` | string | Three-letter ISO 4217 |
| `discountAmountCents` | int | Same as `discount.discountAmountCents` — convenience top-level field |
### Response — 200 OK, code invalid
```json theme={null}
{
"valid": false,
"reason": "code_expired"
}
```
The status code is still **200** when a code is structurally well-formed but not eligible
— the response body is the source of truth (the FE branches on `valid`). The endpoint only
returns non-200 for malformed input (400) or rate-limit (429).
### Reason codes
When `valid: false`, the `reason` field carries a stable enum the FE maps to a
human-readable message.
| `reason` | Suggested FE message |
| ---------------------- | --------------------------------------------------------------- |
| `code_not_found` | "That code isn't recognized. Check the spelling and try again." |
| `code_expired` | "This code has expired." |
| `code_not_yet_active` | "This code isn't active yet. Try again on the start date." |
| `code_exhausted` | "This code has reached its redemption limit." |
| `code_disabled` | "This code has been disabled by the merchant." |
| `min_purchase_not_met` | "Add more to your cart to use this code." |
| `product_not_in_scope` | "This code doesn't apply to the items in your cart." |
The enum is stable — new reason codes will only be added, never renamed. FE clients should
default to a generic "This code can't be applied" string when an unknown reason arrives.
### Error responses
| Status | When |
| ------ | ------------------------------------------------------------------------------- |
| `400` | Malformed body (missing `shopId`, negative `cartTotalCents`, empty `lineItems`) |
| `404` | `shopId` does not resolve to an active shop |
| `429` | Rate limit exceeded for caller IP |
## JavaScript example
```javascript theme={null}
const res = await fetch('https://apiv3.droplinked.com/v2/discounts/validate', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
shopId,
code: enteredCode,
cartTotalCents,
lineItems,
}),
});
const result = await res.json();
if (result.valid) {
setCartDiscount(result.discountAmountCents);
setAppliedCode(result.discount.code);
} else {
setCodeError(reasonToMessage(result.reason));
}
```
## For partner integrators
Partners building custom checkouts on top of droplinked's catalog and checkout backbone
should call `POST /v2/discounts/validate` immediately before submitting the cart to
`POST /v2/checkout/intent`. The pattern is:
1. Customer enters a code in your checkout UI
2. Your storefront calls `POST /v2/discounts/validate` with the current cart shape
3. If `valid: true`, render the post-discount cart total in the summary and pass the
validated code through as the `discountCode` field on `POST /v2/checkout/intent`
4. The checkout-intent resolver redeems the code as part of the same transaction that
creates the order
This pattern guarantees the customer sees the same total at validation, at the cart
summary, and at the PSP authorization step — and that no double-redemption is possible
because redemption only happens at intent submission.
## Related
* [Checkout payment-intent resolver](/api-reference/public/checkout-intent-resolver) — the
chokepoint where validated discount codes are redeemed.
* [Merchants overview](/concepts/for-merchants) — the merchant-facing view of how discounts
fit into the order lifecycle.
* Shop-builder admin discount-creation guide — **coming** (lands once the admin docs page
ships).
# EAS Schemas (Trust Fabric) — Overview
Source: https://docs.droplinked.com/api-reference/public/eas-schemas/overview
On-chain reference for Droplinked's 4-axis trust fabric. Schemas A/B/C/D, registered on Base mainnet, anchor every verifiable claim the platform makes about brands, credit, repayment, and peer trust.
Droplinked's trust fabric is published as **four [EAS](https://attest.org) schemas on Base mainnet**. Every verifiable claim — verified brand, lender underwriting verdict, append-only repayment record, peer trust — is anchored to one of these schemas. The records are public, the issuers are operator-registered, and the reconciler closes the loop on revocations and registry drift.
If you only need aggregate counts (lenders, attestations, service providers), see [`/v2/trust-fabric/stats`](/api-reference/public/trust-fabric-stats). The pages in this group are the **on-chain reference** — UIDs, field tables, and copy-pasteable read examples in viem / ethers / Cast for verifiers that want to read EAS directly without going through Droplinked's API.
## The 4 axes
| Axis | Schema | Issued by | Subject | Revocable | Reference |
| ----- | ----------------------------- | --------------------- | --------------------- | -------------------- | ------------------------------------------------------------------------------------------ |
| **A** | `BrandAttestation` | Droplinked operator | Merchant brand slug | yes | [Schema A — Entity / Brand](/api-reference/public/eas-schemas/schema-a-entity) |
| **B** | `CreditRiskAttestation` | Registered lender | Merchant | yes | [Schema B — Writer / Credit-Risk](/api-reference/public/eas-schemas/schema-b-writer) |
| **C** | `RepaymentHistoryAttestation` | Registered lender | Merchant | **no** (append-only) | [Schema C — Reader / Repayment-History](/api-reference/public/eas-schemas/schema-c-reader) |
| **D** | `CrossAttestation` | Any registered entity | Any registered entity | yes | [Schema D — Reconciler / Cross](/api-reference/public/eas-schemas/schema-d-reconciler) |
## How the axes compose
```
┌──────────────────────────────────────────────┐
│ Droplinked operator │
│ (issues Schema A: brand identity / KYB) │
└────────────────────┬─────────────────────────┘
│ anchors
▼
┌────────────────────┐ ┌──────────────────┐ ┌────────────────────┐
│ LenderRegistry │ │ Merchant entity │ │ServiceProviderReg. │
│ (Schema B/C gate) │ │ (Schema A) │ │ (Schema D gate) │
└────────┬───────────┘ └─────────┬────────┘ └─────────┬──────────┘
│ writes │ subject of │ writes
▼ ▼ ▼
┌─────────────────────────┐ ┌─────────────────────┐ ┌──────────────────────┐
│ Schema B credit-risk │ │ Order / event / │ │ Schema D peer │
│ (lender → merchant) │ │ settlement │ │ cross-attestation │
└──────────┬──────────────┘ └──────────┬──────────┘ └──────────┬───────────┘
│ amortizes │ evidence │ scores
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ Schema C repayment-history │
│ (lender → merchant, append-only running rollup per lender) │
└─────────────────────────────┬────────────────────────────────────┘
│ reconciled every 6h
▼
┌───────────────────────┐
│ Reconciler crons │
│ • mirror registry │
│ status onto B + D │
│ • flip EXPIRED │
│ • sync REVOKED │
└───────────────────────┘
```
Read the flow as: **A** anchors the entity → **B** is a lender's verdict on that entity → **C** is the append-only evidence of how that verdict performed → **D** is a peer cross-check by any registered party. The reconciler **never auto-revokes** — it only mirrors current registry state so verifiers can apply their own policy.
## Network coverage
| Network | Status | EAS contract | Explorer |
| -------------------------- | ------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| **Base mainnet** | live (flipped 2026-06-14) | `0x4200000000000000000000000000000000000021` | [basescan.org](https://basescan.org/address/0x4200000000000000000000000000000000000021) |
| **Base Sepolia** (testnet) | live (development) | `0x4200000000000000000000000000000000000021` | [sepolia.basescan.org](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000021) |
EAS is a [predeploy on every OP-Stack chain](https://docs.attest.org/docs/quick--start/contracts) at the same address; Droplinked's schemas are registered separately per chain. The active chain is configured via `EAS_ACTIVE_CHAIN` (`base` in production) and every `/v2/attestations/*` response includes the chain in its envelope.
## Schema UIDs (Base mainnet, registered 2026-06-14)
| Schema | Mainnet UID | easscan |
| --------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| A — Brand | `0xf16f6f5c8da93edb3f700c7a5cf2f51bbe738eb1c275a7f5bc7657ef6e27e5a3` | [view](https://base.easscan.org/schema/view/0xf16f6f5c8da93edb3f700c7a5cf2f51bbe738eb1c275a7f5bc7657ef6e27e5a3) |
| B — Credit-Risk | `0xbd652f94a034edea8635b1f87d05f2b33b23c0243430c2dae91469103921c7bd` | [view](https://base.easscan.org/schema/view/0xbd652f94a034edea8635b1f87d05f2b33b23c0243430c2dae91469103921c7bd) |
| C — Repayment-History | `0x090a64afc751696ba228f82225f084f73949644921e286108c86d3ea4d37f24c` | [view](https://base.easscan.org/schema/view/0x090a64afc751696ba228f82225f084f73949644921e286108c86d3ea4d37f24c) |
| D — Cross | `0xb076cf65e9547678fe0e0c5ff010b504e7133ffe03977dc5fad2598723e5554e` | [view](https://base.easscan.org/schema/view/0xb076cf65e9547678fe0e0c5ff010b504e7133ffe03977dc5fad2598723e5554e) |
The same axes are also registered on **Base Sepolia** for development. Testnet UIDs differ from mainnet; resolve them at runtime via the `chain` field on any `/v2/attestations/*` response, or via the Droplinked-side admin registry (operator-only).
Schema UIDs are **per-chain**. A Schema B mainnet UID is NOT valid on Sepolia and vice versa. Always pair the UID with the chain when constructing a verifier query.
## Three ways to read
For any schema, you have three independent paths to the same on-chain record:
1. **Droplinked verifier API** — `GET /v2/attestations//`. Returns the latest active attestation with the registry-mirror envelope (`lenderCurrentStatus`, `attestorCurrentStatus`). Best for hot-path reads where you already trust Droplinked's projection.
2. **EAS contract directly** — call `getAttestation(uid)` on `0x4200…0021` via viem / ethers / Cast. Best for independent verification: you read the attestation bytes straight off chain.
3. **EAS GraphQL indexer** — query `https://base.easscan.org/graphql` (or `https://base-sepolia.easscan.org/graphql` for testnet). Best for enumeration: "all Schema B attestations for this merchant", "all Schema D attestations issued by this lender".
Each schema page below documents all three.
## Issuer registries (who is authorized)
A schema UID alone does not prove authority — the **issuing wallet must be currently registered + ACTIVE** in the matching registry. Verifiers SHOULD always cross-check:
| Schema | Registry | Public lookup |
| ------ | ---------------------------------------------------------- | -------------------------------------------------------------------- |
| A | Droplinked operator wallet | (hard-coded; published in each schema page) |
| B, C | `LenderRegistry` | [`GET /v2/lenders/:lenderId`](/api-reference/public/lender-registry) |
| D | `ServiceProviderRegistry` (for `service-provider` issuers) | (admin-only registry; status mirrored onto attestation) |
The reconciler cron mirrors the **current** registry status onto every ACTIVE Schema B + Schema D row every 6 hours (`lenderCurrentStatus` / `attestorCurrentStatus`). A verifier reading the Droplinked API gets this mirror for free; a verifier reading EAS directly should re-check the issuer wallet against the registry.
## Subject anchors (`subjectRootUid`)
Schemas B, C, and D address subjects via a `subjectRootUid` (Schema D also has `issuerRootUid`). The current production scheme is a **labeled string**:
```
"merchant:"
"lender:"
"business-buyer:"
"service-provider:"
```
A future schema-rotation will swap this to `keccak256(entityType || entityId)` bytes32 (already reflected as the persistent `subjectRootUid` field in the verifier API). When that rotation lands, the schema UID changes and legacy attestations remain valid against the legacy UID. Both forms will be queryable side-by-side during the transition window.
## Next
Continue to the per-schema reference pages for fields, ABI types, and copy-pasteable read examples:
* [Schema A — Brand attestation](/api-reference/public/eas-schemas/schema-a-entity)
* [Schema B — Credit-risk attestation](/api-reference/public/eas-schemas/schema-b-writer)
* [Schema C — Repayment-history attestation](/api-reference/public/eas-schemas/schema-c-reader)
* [Schema D — Cross / peer attestation](/api-reference/public/eas-schemas/schema-d-reconciler)
# Schema A — Brand attestation
Source: https://docs.droplinked.com/api-reference/public/eas-schemas/schema-a-entity
On-chain reference for the brand-attestation schema. Anchors a Droplinked-side brand slug to an operator-reviewed KYB record, with chain-of-custody from KYB approval to merchant-portal verifier.
Schema A is the **entity attestation** — Droplinked's on-chain receipt that a brand slug has been operator-reviewed and is who it says it is. It anchors the first axis of the [4-axis trust fabric](/api-reference/public/eas-schemas/overview): everything downstream (lender underwriting, peer cross-attestations) presumes the brand identity has been verified.
| | |
| -------------------- | ---------------------------------------------------------------------------------------------- |
| **Schema name** | `BrandAttestation` |
| **EAS revocable** | yes |
| **Expiry semantics** | none by default (revocation is the lifecycle event) |
| **Issued by** | Droplinked operator wallet |
| **Subject** | Merchant brand slug + shop id |
| **MCP tool** | [`verify_brand_attestation`](/agentic/mcp-server) |
| **Verifier API** | `GET /v2/attestations/brand/:slug` |
| **Lifecycle guide** | [Brand attestation: request → mint → verify](/guides/trust-fabric/brand-attestation-lifecycle) |
## Network & UIDs
| Network | EAS contract | Schema UID | Explorer |
| ------------ | -------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Base mainnet | `0x4200000000000000000000000000000000000021` | `0xf16f6f5c8da93edb3f700c7a5cf2f51bbe738eb1c275a7f5bc7657ef6e27e5a3` | [basescan](https://basescan.org/address/0x4200000000000000000000000000000000000021) · [easscan schema](https://base.easscan.org/schema/view/0xf16f6f5c8da93edb3f700c7a5cf2f51bbe738eb1c275a7f5bc7657ef6e27e5a3) |
| Base Sepolia | `0x4200000000000000000000000000000000000021` | resolve via `chain` field on `/v2/attestations/brand/:slug` | [sepolia.basescan](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000021) |
EAS registry version: **v1.4.0** (EIP-712 typed-data attestations; revocation via `revoke(bytes32 uid)` direct call).
## Field reference
The on-chain payload is ABI-encoded against the schema string:
```
string brandName, uint256 verifiedSince, string kybCohort, string offchainProjectionId
```
| Field | Solidity type | Semantic | Mutability |
| ---------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| `brandName` | `string` | Display name resolved at issuance time. Used by the public verifier surface and any human-readable consumer. | immutable per attestation; a rename re-issues |
| `verifiedSince` | `uint256` | Unix seconds. First KYB-approval timestamp for this brand (snapshot — NOT the issuance time of THIS attestation). | immutable |
| `kybCohort` | `string` | Snapshot of the `KybCohort` enum at issuance (e.g. `"GLOBAL_STANDARD"`, `"GCC_ENHANCED"`). Cohort drift is captured by re-issuance, not in-place mutation. | immutable per attestation |
| `offchainProjectionId` | `string` | Mongo ObjectId of the underlying `KybRecord`. Allows a verifier with operator-side access to trace back to the supporting documentation; for public verifiers it is opaque. | immutable |
EAS envelope fields (set by the contract, not by the schema):
| Envelope field | Type | Semantic |
| ---------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `uid` | `bytes32` | The attestation UID. Globally unique per chain. Quote this from MCP / API output. |
| `schema` | `bytes32` | Schema UID (see table above). |
| `attester` | `address` | Droplinked operator signing wallet. Mainnet issuer wallet is published on the [trust-fabric overview](/concepts/trust-fabric). |
| `recipient` | `address` | The shop's recipient wallet if configured; otherwise a deterministic `shopId`-derived address. |
| `time` | `uint64` | Block-mined issuance time. |
| `expirationTime` | `uint64` | `0` (no expiry). |
| `revocationTime` | `uint64` | `0` if active; block time of `revoke()` otherwise. |
| `revocable` | `bool` | `true`. |
| `data` | `bytes` | ABI-encoded payload per the schema string above. |
## Read via Droplinked API
```bash theme={null}
curl -s https://apiv3.droplinked.com/v2/attestations/brand/my-shop-slug | jq .
```
```json theme={null}
{
"found": true,
"chain": "base",
"attestationUid": "0x9a3c…ee01",
"schemaUid": "0xf16f…e5a3",
"issuer": "0xD5F6FB7b6E71DD7F609b8442951444E5a5C76cce",
"recipient": "0x…",
"issuedAt": "2026-06-14T18:08:00Z",
"status": "ACTIVE",
"attestationData": {
"brandName": "My Shop",
"verifiedSince": "2026-03-02T00:00:00Z",
"kybCohort": "GLOBAL_STANDARD",
"offchainProjectionId": "65a3…ef01"
}
}
```
## Read via viem (TypeScript)
```ts theme={null}
import { createPublicClient, http, parseAbi, decodeAbiParameters } from "viem";
import { base } from "viem/chains";
const EAS = "0x4200000000000000000000000000000000000021" as const;
const SCHEMA_A = "0xf16f6f5c8da93edb3f700c7a5cf2f51bbe738eb1c275a7f5bc7657ef6e27e5a3" as const;
const client = createPublicClient({ chain: base, transport: http() });
const easAbi = parseAbi([
"function getAttestation(bytes32 uid) view returns (tuple(bytes32 uid, bytes32 schema, uint64 time, uint64 expirationTime, uint64 revocationTime, bytes32 refUID, address recipient, address attester, bool revocable, bytes data))",
]);
const uid = "0x…"; // UID returned by /v2/attestations/brand/:slug
const att = await client.readContract({
address: EAS,
abi: easAbi,
functionName: "getAttestation",
args: [uid],
});
// att.schema must equal SCHEMA_A; if not, this is a different schema's UID.
if (att.schema !== SCHEMA_A) throw new Error("not a Schema A attestation");
const [brandName, verifiedSince, kybCohort, offchainProjectionId] =
decodeAbiParameters(
[
{ name: "brandName", type: "string" },
{ name: "verifiedSince", type: "uint256" },
{ name: "kybCohort", type: "string" },
{ name: "offchainProjectionId", type: "string" },
],
att.data,
);
const isActive = att.revocationTime === 0n;
console.log({ brandName, verifiedSince, kybCohort, offchainProjectionId, isActive });
```
## Read via ethers (TypeScript)
```ts theme={null}
import { JsonRpcProvider, Contract, AbiCoder } from "ethers";
const EAS = "0x4200000000000000000000000000000000000021";
const SCHEMA_A = "0xf16f6f5c8da93edb3f700c7a5cf2f51bbe738eb1c275a7f5bc7657ef6e27e5a3";
const provider = new JsonRpcProvider("https://mainnet.base.org");
const eas = new Contract(
EAS,
[
"function getAttestation(bytes32 uid) view returns (tuple(bytes32 uid, bytes32 schema, uint64 time, uint64 expirationTime, uint64 revocationTime, bytes32 refUID, address recipient, address attester, bool revocable, bytes data))",
],
provider,
);
const uid = "0x…";
const att = await eas.getAttestation(uid);
if (att.schema.toLowerCase() !== SCHEMA_A) throw new Error("schema mismatch");
const [brandName, verifiedSince, kybCohort, offchainProjectionId] =
AbiCoder.defaultAbiCoder().decode(
["string", "uint256", "string", "string"],
att.data,
);
const isActive = att.revocationTime === 0n;
console.log({ brandName, verifiedSince, kybCohort, offchainProjectionId, isActive });
```
## Read via Cast (Foundry)
```bash theme={null}
# Replace UID with the value returned by /v2/attestations/brand/:slug.
cast call 0x4200000000000000000000000000000000000021 \
"getAttestation(bytes32)((bytes32,bytes32,uint64,uint64,uint64,bytes32,address,address,bool,bytes))" \
0x9a3cffffffffffffffffffffffffffffffffffffffffffffffffffffffffee01 \
--rpc-url https://mainnet.base.org
# Decode the `data` field (last element) as the schema payload:
cast abi-decode \
"x(string,uint256,string,string)" \
0x… # the `data` hex from the previous call
```
## Indexer access (EAS GraphQL)
EAS publishes a public GraphQL indexer per chain. Use it to enumerate without RPC round-trips.
* Base mainnet playground: [https://base.easscan.org/graphql](https://base.easscan.org/graphql)
* Base Sepolia playground: [https://base-sepolia.easscan.org/graphql](https://base-sepolia.easscan.org/graphql)
### Common queries
**All Schema A attestations for a brand slug** — enumerate by attester (Droplinked operator) + schema, then resolve `data` client-side to filter by brandName. There is no on-chain index on brandName; for scale, use the Droplinked verifier API.
```graphql theme={null}
query LatestForAttester {
attestations(
where: {
schemaId: { equals: "0xf16f6f5c8da93edb3f700c7a5cf2f51bbe738eb1c275a7f5bc7657ef6e27e5a3" }
attester: { equals: "0xD5F6FB7b6E71DD7F609b8442951444E5a5C76cce" }
revoked: { equals: false }
}
orderBy: { time: desc }
take: 50
) {
id
time
recipient
revocationTime
data
}
}
```
**Single attestation by UID**:
```graphql theme={null}
query OneByUid {
attestation(where: { id: "0x9a3c…ee01" }) {
id
schemaId
attester
recipient
time
revocationTime
data
}
}
```
## Trust assumptions
A Schema A attestation is authoritative if and only if:
1. The on-chain `attester` is the **Droplinked operator wallet**: `0xD5F6FB7b6E71DD7F609b8442951444E5a5C76cce` (Base mainnet).
2. The on-chain `schema` matches the mainnet UID for Schema A (see top of page).
3. `revocationTime == 0` AND `expirationTime == 0` (Schema A is issued without expiry by default).
4. If issued via a future operator-wallet rotation, the wallet was the registered operator at issuance time — check the `time` field against the operator-rotation log.
KMS-backed signer rotation is queued (tracking: backend issue #1718). Until it ships, the mainnet issuer is the personal operator wallet listed above. After rotation, this page will be updated and the legacy wallet will remain valid for the historical window.
## Related
* [Trust Fabric overview](/concepts/trust-fabric)
* [Brand attestation lifecycle guide](/guides/trust-fabric/brand-attestation-lifecycle)
* [Brand attestation request endpoint](/api-reference/public/brand-attestation-request)
* [`/v2/trust-fabric/stats` — aggregate counts](/api-reference/public/trust-fabric-stats)
# Schema B — Credit-risk attestation
Source: https://docs.droplinked.com/api-reference/public/eas-schemas/schema-b-writer
On-chain reference for the credit-risk schema. A registered lender's underwriting verdict on a merchant — credit tier, line ceiling, term, rate, methodology hash.
Schema B is the **writer attestation** — a registered lender's signed verdict on a merchant. Every approved lending application that mints on-chain mints a Schema B row, anchoring the line ceiling, term, rate, and methodology hash for downstream consumers (order credit-leg, attribution credit-leg, lender-agent quote enrichment).
| | |
| -------------------- | ---------------------------------------------------------------------------------------- |
| **Schema name** | `CreditRiskAttestation` |
| **EAS revocable** | yes |
| **Expiry semantics** | required (`expiresAt` is mandatory; lender re-underwrites at expiry) |
| **Issued by** | A wallet currently `ACTIVE` in [`LenderRegistry`](/api-reference/public/lender-registry) |
| **Subject** | Merchant (`subjectRootUid`) |
| **MCP tool** | [`verify_credit_risk`](/agentic/mcp-server) |
| **Verifier API** | `GET /v2/attestations/credit-risk/:merchantId` |
| **Composite read** | [`GET /v2/underwriting-signals/:merchantId`](/api-reference/public/underwriting-signals) |
## Network & UIDs
| Network | EAS contract | Schema UID | Explorer |
| ------------ | -------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Base mainnet | `0x4200000000000000000000000000000000000021` | `0xbd652f94a034edea8635b1f87d05f2b33b23c0243430c2dae91469103921c7bd` | [basescan](https://basescan.org/address/0x4200000000000000000000000000000000000021) · [easscan schema](https://base.easscan.org/schema/view/0xbd652f94a034edea8635b1f87d05f2b33b23c0243430c2dae91469103921c7bd) |
| Base Sepolia | `0x4200000000000000000000000000000000000021` | resolve via `chain` field on `/v2/attestations/credit-risk/:merchantId` | [sepolia.basescan](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000021) |
EAS registry version: **v1.4.0**.
## Field reference
ABI schema string:
```
string subjectRootUid, uint8 creditTier, uint64 maxCreditLineUsdCents,
uint32 termDays, uint32 rateBps, bytes32 methodologyHash, uint256 expiresAt
```
| Field | Solidity type | Semantic | Mutability |
| ----------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `subjectRootUid` | `string` | Subject anchor. Production form: `"merchant:"`. Post-rotation: `keccak256(entityType\|\|entityId)` bytes32 (legacy + new coexist during transition). | immutable per attestation |
| `creditTier` | `uint8` | Resolved credit tier at issuance. Encoded T0=0 / T1=1 / T2=2 / T3=3. Snapshot from `CreditTierMappingService` at mint time. | immutable per attestation |
| `maxCreditLineUsdCents` | `uint64` | Maximum credit line in USD cents (`getLineCeilingUsd × 100`). | immutable per attestation |
| `termDays` | `uint32` | Quoted repayment term in days. DTO layer enforces 1-1800 (5y) before mint. | immutable per attestation |
| `rateBps` | `uint32` | Quoted APR in basis points (`850` = 8.50%). | immutable per attestation |
| `methodologyHash` | `bytes32` | `keccak256` of the lender's published underwriting methodology document. Cross-references the [methodology registry](/api-reference/public/methodology-registry). | immutable per attestation |
| `expiresAt` | `uint256` | Unix seconds. Required; a re-underwrite mints a new attestation. | immutable per attestation |
EAS envelope fields:
| Envelope field | Type | Semantic |
| ---------------- | --------- | ---------------------------------------------------------------------------------------- |
| `uid` | `bytes32` | Attestation UID. |
| `schema` | `bytes32` | Schema UID (above). |
| `attester` | `address` | Lender's on-chain signing wallet. Must be currently `ACTIVE` in `LenderRegistry`. |
| `recipient` | `address` | Conventionally the merchant's wallet or `0x0` (subject identity is in `subjectRootUid`). |
| `time` | `uint64` | Block-mined issuance time. |
| `expirationTime` | `uint64` | Mirrors `expiresAt`. |
| `revocationTime` | `uint64` | `0` if active. |
| `revocable` | `bool` | `true`. |
| `data` | `bytes` | ABI-encoded payload per the schema string above. |
## Lender-registry mirror (`lenderCurrentStatus`)
The reconciler sweeps every 6 hours and mirrors the **current** `LenderRegistry` status onto every ACTIVE Schema B row. A verifier reading the Droplinked API sees:
```json theme={null}
{
"status": "ACTIVE",
"lenderCurrentStatus": "SUSPENDED",
"lenderCurrentStatusAt": "2026-06-14T01:00:00Z"
}
```
The reconciler **never auto-revokes**. A Schema B attestation that's still on-chain ACTIVE but issued by a now-SUSPENDED lender is honored or not per verifier policy. The MCP `verify_credit_risk` tool and the verifier API both expose this mirror.
## Read via Droplinked API
```bash theme={null}
curl -s https://apiv3.droplinked.com/v2/attestations/credit-risk/6a0001a08692425bd9fd571b | jq .
```
```json theme={null}
{
"found": true,
"chain": "base",
"attestations": [
{
"attestationUid": "0xab12…cd34",
"schemaUid": "0xbd65…c7bd",
"lenderId": "crediblex-uae",
"issuerWallet": "0x…",
"creditTier": "T2",
"maxCreditLineUsdCents": 25000000,
"termDays": 90,
"rateBps": 1250,
"methodologyHash": "0x…",
"issuedAt": "2026-06-14T12:00:00Z",
"expiresAt": "2026-12-14T12:00:00Z",
"status": "ACTIVE",
"lenderCurrentStatus": "ACTIVE",
"lenderCurrentStatusAt": "2026-06-14T18:00:00Z"
}
]
}
```
## Read via viem (TypeScript)
```ts theme={null}
import { createPublicClient, http, parseAbi, decodeAbiParameters } from "viem";
import { base } from "viem/chains";
const EAS = "0x4200000000000000000000000000000000000021" as const;
const SCHEMA_B = "0xbd652f94a034edea8635b1f87d05f2b33b23c0243430c2dae91469103921c7bd" as const;
const client = createPublicClient({ chain: base, transport: http() });
const easAbi = parseAbi([
"function getAttestation(bytes32 uid) view returns (tuple(bytes32 uid, bytes32 schema, uint64 time, uint64 expirationTime, uint64 revocationTime, bytes32 refUID, address recipient, address attester, bool revocable, bytes data))",
]);
const uid = "0x…"; // UID from /v2/attestations/credit-risk/:merchantId
const att = await client.readContract({
address: EAS,
abi: easAbi,
functionName: "getAttestation",
args: [uid],
});
if (att.schema !== SCHEMA_B) throw new Error("not a Schema B attestation");
const [
subjectRootUid,
creditTier,
maxCreditLineUsdCents,
termDays,
rateBps,
methodologyHash,
expiresAt,
] = decodeAbiParameters(
[
{ name: "subjectRootUid", type: "string" },
{ name: "creditTier", type: "uint8" },
{ name: "maxCreditLineUsdCents", type: "uint64" },
{ name: "termDays", type: "uint32" },
{ name: "rateBps", type: "uint32" },
{ name: "methodologyHash", type: "bytes32" },
{ name: "expiresAt", type: "uint256" },
],
att.data,
);
const tierLabel = ["T0", "T1", "T2", "T3"][Number(creditTier)];
const isActiveOnChain =
att.revocationTime === 0n && BigInt(Math.floor(Date.now() / 1000)) < expiresAt;
console.log({ subjectRootUid, tier: tierLabel, maxCreditLineUsdCents, termDays, rateBps, isActiveOnChain });
```
## Read via ethers (TypeScript)
```ts theme={null}
import { JsonRpcProvider, Contract, AbiCoder } from "ethers";
const EAS = "0x4200000000000000000000000000000000000021";
const SCHEMA_B = "0xbd652f94a034edea8635b1f87d05f2b33b23c0243430c2dae91469103921c7bd";
const provider = new JsonRpcProvider("https://mainnet.base.org");
const eas = new Contract(
EAS,
[
"function getAttestation(bytes32 uid) view returns (tuple(bytes32 uid, bytes32 schema, uint64 time, uint64 expirationTime, uint64 revocationTime, bytes32 refUID, address recipient, address attester, bool revocable, bytes data))",
],
provider,
);
const uid = "0x…";
const att = await eas.getAttestation(uid);
if (att.schema.toLowerCase() !== SCHEMA_B) throw new Error("schema mismatch");
const decoded = AbiCoder.defaultAbiCoder().decode(
["string", "uint8", "uint64", "uint32", "uint32", "bytes32", "uint256"],
att.data,
);
const [subjectRootUid, creditTier, maxCreditLineUsdCents, termDays, rateBps, methodologyHash, expiresAt] = decoded;
console.log({ subjectRootUid, creditTier, maxCreditLineUsdCents, termDays, rateBps, expiresAt });
```
## Read via Cast (Foundry)
```bash theme={null}
cast call 0x4200000000000000000000000000000000000021 \
"getAttestation(bytes32)((bytes32,bytes32,uint64,uint64,uint64,bytes32,address,address,bool,bytes))" \
0xab12ffffffffffffffffffffffffffffffffffffffffffffffffffffffffcd34 \
--rpc-url https://mainnet.base.org
# Decode the schema payload from the `data` field of the above:
cast abi-decode \
"x(string,uint8,uint64,uint32,uint32,bytes32,uint256)" \
0x…
```
## Indexer access (EAS GraphQL)
* Base mainnet: [https://base.easscan.org/graphql](https://base.easscan.org/graphql)
* Base Sepolia: [https://base-sepolia.easscan.org/graphql](https://base-sepolia.easscan.org/graphql)
**All Schema B attestations issued by a specific lender wallet, newest first:**
```graphql theme={null}
query LenderBook($wallet: String!) {
attestations(
where: {
schemaId: { equals: "0xbd652f94a034edea8635b1f87d05f2b33b23c0243430c2dae91469103921c7bd" }
attester: { equals: $wallet }
}
orderBy: { time: desc }
take: 100
) {
id
time
revocationTime
expirationTime
recipient
data
}
}
```
**All Schema B attestations for a merchant** — there is no on-chain index on `subjectRootUid`; for scale, query the Droplinked API (`GET /v2/attestations/credit-risk/:merchantId`) which serves an indexed projection.
## Trust assumptions
A Schema B attestation is authoritative if and only if:
1. The on-chain `schema` matches the mainnet UID above.
2. The on-chain `attester` is currently `ACTIVE` in [`LenderRegistry`](/api-reference/public/lender-registry). Check via `GET /v2/lenders/:lenderId` or read the Droplinked-API envelope's `lenderCurrentStatus` mirror.
3. `revocationTime == 0` AND `expirationTime > now`.
4. The `methodologyHash` resolves to an `ACTIVE` row in the [methodology registry](/api-reference/public/methodology-registry) for that lender — the lender's published methodology document is the explanation surface for the verdict.
A Schema B attestation issued by a lender that has since been **SUSPENDED** or **ARCHIVED** remains on-chain ACTIVE — the chain has no opinion on the lender's current standing. The Droplinked reconciler exposes the current registry status via `lenderCurrentStatus`. **Verifier-side policy** decides whether to honor it. The agentic PM shadow protocol is explicit: this is a verifier decision, not a reconciler decision.
## Related
* [Trust Fabric overview](/concepts/trust-fabric)
* [Schemas overview](/api-reference/public/eas-schemas/overview)
* [Lender Registry Lookup](/api-reference/public/lender-registry)
* [Methodology Registry Lookup](/api-reference/public/methodology-registry)
* [`/v2/underwriting-signals/:merchantId`](/api-reference/public/underwriting-signals) — composite Schema B + C read
# Schema C — Repayment-history attestation
Source: https://docs.droplinked.com/api-reference/public/eas-schemas/schema-c-reader
On-chain reference for the append-only repayment-history schema. Per-event evidence powering merchant credit-tier upgrades and the new attestationCoveragePctBps field on /v2/merchant/orders.
Schema C is the **reader / repayment-history attestation** — a registered lender's append-only record of a merchant's settlement performance against credit lines that lender has issued. Every settlement event mints a new row; the latest row per `(merchantId, lenderId)` is the active view, and the chain preserves the full history.
This is the per-order evidence schema. It powers the `attestationCoveragePctBps` field on [`/v2/merchant/orders`](/api-reference/public/monetization/orders) — the fraction of credit-leg-bearing orders that have a corresponding Schema C attestation, in basis points.
| | |
| -------------------- | ---------------------------------------------------------------------------------------- |
| **Schema name** | `RepaymentHistoryAttestation` |
| **EAS revocable** | **no** — append-only |
| **Expiry semantics** | none |
| **Issued by** | A wallet currently `ACTIVE` in [`LenderRegistry`](/api-reference/public/lender-registry) |
| **Subject** | Merchant (`subjectRootUid`) |
| **MCP tool** | [`verify_repayment_history`](/agentic/mcp-server) |
| **Verifier API** | `GET /v2/attestations/repayment-history/:merchantId` |
| **Composite read** | [`GET /v2/underwriting-signals/:merchantId`](/api-reference/public/underwriting-signals) |
## Why append-only
Schema C is registered as `revocable: false` on chain. Corrections are issued as a **new attestation with updated counts**; the chain preserves the full history; latest-by-`attestedAt` per `(merchantId, lenderId)` wins for read-time queries. The counters are monotonically non-decreasing — `settledOnTimeCount`, `lateCount`, `defaultCount`, `totalLinesUsdCents` all only ever go up.
## Network & UIDs
| Network | EAS contract | Schema UID | Explorer |
| ------------ | -------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Base mainnet | `0x4200000000000000000000000000000000000021` | `0x090a64afc751696ba228f82225f084f73949644921e286108c86d3ea4d37f24c` | [basescan](https://basescan.org/address/0x4200000000000000000000000000000000000021) · [easscan schema](https://base.easscan.org/schema/view/0x090a64afc751696ba228f82225f084f73949644921e286108c86d3ea4d37f24c) |
| Base Sepolia | `0x4200000000000000000000000000000000000021` | resolve via `chain` field on `/v2/attestations/repayment-history/:merchantId` | [sepolia.basescan](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000021) |
EAS registry version: **v1.4.0**.
## Field reference
ABI schema string:
```
string subjectRootUid, uint64 totalLinesUsdCents, uint32 settledOnTimeCount,
uint32 lateCount, uint32 defaultCount, uint256 lastSettlementAt
```
| Field | Solidity type | Semantic | Mutability |
| -------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `subjectRootUid` | `string` | Subject anchor. Production form: `"merchant:"`. | immutable per attestation |
| `totalLinesUsdCents` | `uint64` | Running total credit drawn across the merchant-lender relationship. Monotonically non-decreasing. | snapshot — monotonic across the chain of attestations |
| `settledOnTimeCount` | `uint32` | Running count of settlement events that settled within term. Monotonically non-decreasing. | snapshot — monotonic |
| `lateCount` | `uint32` | Running count of settlements past `expectedRepaymentDate` but within the default cutoff (default 30d post-due). Monotonically non-decreasing. | snapshot — monotonic |
| `defaultCount` | `uint32` | Running count of credit lines that defaulted (>30d past due or written off). Monotonically non-decreasing. | snapshot — monotonic |
| `lastSettlementAt` | `uint256` | Unix seconds of the most recent settlement event captured by this row. Read consumers compute "freshest signal per lender" via `MAX(lastSettlementAt)`. | snapshot |
EAS envelope fields:
| Envelope field | Type | Semantic |
| ---------------- | --------- | ------------------------------------------------ |
| `uid` | `bytes32` | Attestation UID. |
| `schema` | `bytes32` | Schema UID (above). |
| `attester` | `address` | Lender's on-chain signing wallet. |
| `recipient` | `address` | Conventionally merchant wallet or `0x0`. |
| `time` | `uint64` | Block-mined `attestedAt`. |
| `expirationTime` | `uint64` | `0` (no expiry). |
| `revocationTime` | `uint64` | Always `0` — Schema C is non-revocable. |
| `revocable` | `bool` | `false`. |
| `data` | `bytes` | ABI-encoded payload per the schema string above. |
## Order-level evidence — `attestationCoveragePctBps`
The merchant orders endpoint exposes a top-line coverage metric derived from Schema C:
```json theme={null}
{
"summary": {
"attestationCoveragePctBps": 9650,
"creditLegBearingOrdersCount": 200,
"ordersWithSchemaCAttestationCount": 193
}
}
```
`9650` bps = 96.50% of credit-leg-bearing orders have a matching Schema C attestation (via `orderId` cross-reference). A drop in coverage means the lender's settlement-event emitter is lagging or has stopped — it's a freshness probe for the merchant's underwriting signal stream.
## Reading running totals: snapshot vs delta
A single Schema C row is a **cumulative snapshot** as of `lastSettlementAt`. To compute the delta added by the most recent settlement, fetch the previous attestation for the same `(merchantId, lenderId)` and subtract counters. The Droplinked API's `verify_repayment_history` already does this aggregation:
```json theme={null}
{
"merchantId": "6a0001a08692425bd9fd571b",
"perLender": [
{
"lenderId": "crediblex-uae",
"latest": {
"attestationUid": "0x…",
"totalLinesUsdCents": 125000000,
"settledOnTimeCount": 12,
"lateCount": 1,
"defaultCount": 0,
"lastSettlementAt": "2026-06-14T11:00:00Z"
}
}
],
"aggregate": {
"totalLinesUsdCents": 125000000,
"settledOnTimeCount": 12,
"lateCount": 1,
"defaultCount": 0
}
}
```
The aggregate sums latest-row counters across lenders for credit-tier upgrade decisions: T1→T2 requires 3+ settled-on-time; T2→T3 requires 10+ settled with 0 defaults in 24mo.
## Read via Droplinked API
```bash theme={null}
curl -s https://apiv3.droplinked.com/v2/attestations/repayment-history/6a0001a08692425bd9fd571b | jq .
```
## Read via viem (TypeScript)
```ts theme={null}
import { createPublicClient, http, parseAbi, decodeAbiParameters } from "viem";
import { base } from "viem/chains";
const EAS = "0x4200000000000000000000000000000000000021" as const;
const SCHEMA_C = "0x090a64afc751696ba228f82225f084f73949644921e286108c86d3ea4d37f24c" as const;
const client = createPublicClient({ chain: base, transport: http() });
const easAbi = parseAbi([
"function getAttestation(bytes32 uid) view returns (tuple(bytes32 uid, bytes32 schema, uint64 time, uint64 expirationTime, uint64 revocationTime, bytes32 refUID, address recipient, address attester, bool revocable, bytes data))",
]);
const uid = "0x…"; // UID from /v2/attestations/repayment-history/:merchantId
const att = await client.readContract({
address: EAS,
abi: easAbi,
functionName: "getAttestation",
args: [uid],
});
if (att.schema !== SCHEMA_C) throw new Error("not a Schema C attestation");
const [
subjectRootUid,
totalLinesUsdCents,
settledOnTimeCount,
lateCount,
defaultCount,
lastSettlementAt,
] = decodeAbiParameters(
[
{ name: "subjectRootUid", type: "string" },
{ name: "totalLinesUsdCents", type: "uint64" },
{ name: "settledOnTimeCount", type: "uint32" },
{ name: "lateCount", type: "uint32" },
{ name: "defaultCount", type: "uint32" },
{ name: "lastSettlementAt", type: "uint256" },
],
att.data,
);
console.log({
subjectRootUid,
totalLinesUsdCents,
settledOnTimeCount,
lateCount,
defaultCount,
lastSettlementAt,
});
```
## Read via ethers (TypeScript)
```ts theme={null}
import { JsonRpcProvider, Contract, AbiCoder } from "ethers";
const EAS = "0x4200000000000000000000000000000000000021";
const SCHEMA_C = "0x090a64afc751696ba228f82225f084f73949644921e286108c86d3ea4d37f24c";
const provider = new JsonRpcProvider("https://mainnet.base.org");
const eas = new Contract(
EAS,
[
"function getAttestation(bytes32 uid) view returns (tuple(bytes32 uid, bytes32 schema, uint64 time, uint64 expirationTime, uint64 revocationTime, bytes32 refUID, address recipient, address attester, bool revocable, bytes data))",
],
provider,
);
const uid = "0x…";
const att = await eas.getAttestation(uid);
if (att.schema.toLowerCase() !== SCHEMA_C) throw new Error("schema mismatch");
const [subjectRootUid, totalLinesUsdCents, settledOnTimeCount, lateCount, defaultCount, lastSettlementAt] =
AbiCoder.defaultAbiCoder().decode(
["string", "uint64", "uint32", "uint32", "uint32", "uint256"],
att.data,
);
console.log({ subjectRootUid, totalLinesUsdCents, settledOnTimeCount, lateCount, defaultCount, lastSettlementAt });
```
## Read via Cast (Foundry)
```bash theme={null}
cast call 0x4200000000000000000000000000000000000021 \
"getAttestation(bytes32)((bytes32,bytes32,uint64,uint64,uint64,bytes32,address,address,bool,bytes))" \
0x…UID… \
--rpc-url https://mainnet.base.org
cast abi-decode \
"x(string,uint64,uint32,uint32,uint32,uint256)" \
0x…data-field…
```
## Indexer access (EAS GraphQL)
* Base mainnet: [https://base.easscan.org/graphql](https://base.easscan.org/graphql)
* Base Sepolia: [https://base-sepolia.easscan.org/graphql](https://base-sepolia.easscan.org/graphql)
**All Schema C attestations a lender has emitted, newest first:**
```graphql theme={null}
query LenderRepaymentBook($wallet: String!) {
attestations(
where: {
schemaId: { equals: "0x090a64afc751696ba228f82225f084f73949644921e286108c86d3ea4d37f24c" }
attester: { equals: $wallet }
}
orderBy: { time: desc }
take: 200
) {
id
time
recipient
data
}
}
```
**All Schema C attestations referencing a specific order** — the on-chain `orderId` is part of the off-chain mirror, not the EAS payload. Use `GET /v2/attestations/repayment-history/:merchantId` and filter client-side on `orderId`, or use the Droplinked-side admin lineage endpoint.
## Trust assumptions
A Schema C attestation is authoritative if and only if:
1. The on-chain `schema` matches the mainnet UID above.
2. The on-chain `attester` is `ACTIVE` in [`LenderRegistry`](/api-reference/public/lender-registry).
3. `revocable == false` (sanity check — a Schema C row that claims `revocable == true` is malformed).
4. Counters are **monotonically non-decreasing** relative to the prior attestation in the same `(merchantId, lenderId)` chain. A row that decreases any counter is either malformed or a write-bug; do not honor it. The Droplinked-side issuer enforces this off-chain monotonic guard before mint.
Schema C does not have a `lenderCurrentStatus` mirror (Schema C is append-only history, so a status flip on a lender doesn't change a historical fact). To assess whether to consume a lender's Schema C stream **right now**, consult [`GET /v2/lenders/:lenderId`](/api-reference/public/lender-registry) directly.
## Related
* [Trust Fabric overview](/concepts/trust-fabric)
* [Schemas overview](/api-reference/public/eas-schemas/overview)
* [Schema B — Credit-risk attestation](/api-reference/public/eas-schemas/schema-b-writer)
* [`/v2/merchant/orders` — `attestationCoveragePctBps`](/api-reference/public/monetization/orders)
* [`/v2/underwriting-signals/:merchantId`](/api-reference/public/underwriting-signals)
# Schema D — Cross / peer attestation
Source: https://docs.droplinked.com/api-reference/public/eas-schemas/schema-d-reconciler
On-chain reference for the peer-trust schema. Any registered entity attests any other entity with a 0-100 trust score and short basis — the reconciler closes the loop on disputes by mirroring registry status onto the attestation envelope.
Schema D is the **reconciler / cross-attestation** — peer trust expressed on chain. Unlike Schemas A (operator-issued brand identity), B (lender-issued credit verdict), and C (lender-issued repayment history), Schema D is open to **any registered entity attesting any other registered entity** with a 0-100 trust score and a short free-text `basis`.
This is the dispute-loop surface. When a merchant disputes a credit-risk verdict, when a service provider cross-attests inventory custody, when a business buyer attests a merchant's reliability — those are all Schema D rows. The reconciler closes the loop by mirroring **current** registry status onto every ACTIVE Schema D attestation every 6 hours, so verifiers can decide whether to honor an attestation issued by a party whose standing has since changed.
| | |
| -------------------- | ---------------------------------------------------------------------------------------------- |
| **Schema name** | `CrossAttestation` |
| **EAS revocable** | yes |
| **Expiry semantics** | none |
| **Issued by** | Any registered `merchant` / `lender` / `business-buyer` / `service-provider` |
| **Subject** | Any registered `merchant` / `lender` / `business-buyer` / `service-provider` |
| **MCP tool** | [`verify_cross_attestation`](/agentic/mcp-server), [`get_trust_dossier`](/agentic/mcp-server) |
| **Verifier API** | `GET /v2/attestations/cross/subject/:rootUid` and `GET /v2/attestations/cross/issuer/:rootUid` |
## Network & UIDs
| Network | EAS contract | Schema UID | Explorer |
| ------------ | -------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Base mainnet | `0x4200000000000000000000000000000000000021` | `0xb076cf65e9547678fe0e0c5ff010b504e7133ffe03977dc5fad2598723e5554e` | [basescan](https://basescan.org/address/0x4200000000000000000000000000000000000021) · [easscan schema](https://base.easscan.org/schema/view/0xb076cf65e9547678fe0e0c5ff010b504e7133ffe03977dc5fad2598723e5554e) |
| Base Sepolia | `0x4200000000000000000000000000000000000021` | resolve via `chain` field on `/v2/attestations/cross/*` | [sepolia.basescan](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000021) |
EAS registry version: **v1.4.0**.
## Field reference
ABI schema string:
```
string issuerRootUid, string subjectRootUid, uint8 trustScore, string basis, uint256 attestedAt
```
| Field | Solidity type | Semantic | Mutability |
| ---------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `issuerRootUid` | `string` | Attester anchor. Form: `":"` where `entityType ∈ {merchant, lender, business-buyer, service-provider}`. | immutable per attestation |
| `subjectRootUid` | `string` | Subject anchor. Same form as `issuerRootUid`. | immutable per attestation |
| `trustScore` | `uint8` | 0-100. Semantically **not** canonicalized by Droplinked — a 70 from a lender and a 70 from a business-buyer are not directly comparable. Always combine with `basis` and `issuerEntityType` when consuming. | immutable per attestation |
| `basis` | `string` | Free-text basis for the score. Capped at 512 bytes (\~\$0.08 mainnet gas). | immutable per attestation |
| `attestedAt` | `uint256` | Unix seconds. Block-mined issuance time. | immutable |
EAS envelope fields:
| Envelope field | Type | Semantic |
| ---------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uid` | `bytes32` | Attestation UID. |
| `schema` | `bytes32` | Schema UID (above). |
| `attester` | `address` | Issuer's on-chain signing wallet. For `service-provider` issuers this is the partner's operator wallet (currently `ACTIVE` in `ServiceProviderRegistry`). |
| `recipient` | `address` | Subject's wallet or `0x0` (subject identity is in `subjectRootUid`). |
| `time` | `uint64` | Mirrors `attestedAt`. |
| `expirationTime` | `uint64` | `0` (no expiry). |
| `revocationTime` | `uint64` | `0` if active. |
| `revocable` | `bool` | `true`. |
| `data` | `bytes` | ABI-encoded payload per the schema string above. |
## Registry-status mirror (`attestorCurrentStatus`)
The reconciler sweeps every 6 hours and mirrors the **current** issuer registry status onto every ACTIVE Schema D row. This is how Schema D closes the dispute loop:
```json theme={null}
{
"status": "ACTIVE",
"attestorCurrentStatus": "SUSPENDED",
"attestorCurrentStatusAt": "2026-06-14T01:00:00Z"
}
```
For `service-provider` issuers, this mirrors `ServiceProviderRegistry`. For other issuer types (merchant, lender, business-buyer), this remains `null` until those registry lookups are wired into the reconciler.
The reconciler **never auto-revokes**. A Schema D attestation issued by a service provider that has since been **SUSPENDED** (e.g., contract terminated, custody dispute) is still on-chain ACTIVE — the chain has no opinion. Verifier policy decides whether to honor it. The MCP `get_trust_dossier` tool exposes the mirror in its envelope.
## How Schema D closes the loop on disputes
The 4-axis trust fabric is open on the inputs (lenders write B + C, operators write A) but the **resolution loop is also open**: a disputing party writes a Schema D attestation referencing the disputed record. Two patterns are observed in production:
1. **Counter-attestation** — a merchant disputes a Schema B verdict by minting a Schema D row with `subjectRootUid = "lender:"`, `trustScore` low, and `basis` citing the disputed UID. Subsequent verifiers see both records and apply their own policy.
2. **Service-provider attestation** — a WMS partner (e.g., Stor'd) cross-attests inventory custody. This is independent evidence feeding back into the lender's Schema B re-underwrite at next term. The `service-provider` issuer class is registered in `ServiceProviderRegistry`; the partner's signing wallet must be `ACTIVE` for the attestation to be honored.
The reconciler doesn't take sides. It only ensures that **at read time**, a verifier sees the current registry standing of every party in the dispute graph.
## Read via Droplinked API
By subject — "what has anyone said about this entity":
```bash theme={null}
curl -s "https://apiv3.droplinked.com/v2/attestations/cross/subject/merchant:6a0001a08692425bd9fd571b" | jq .
```
By issuer — "what has this entity said about anyone":
```bash theme={null}
curl -s "https://apiv3.droplinked.com/v2/attestations/cross/issuer/lender:crediblex-uae" | jq .
```
```json theme={null}
{
"found": true,
"chain": "base",
"count": 2,
"attestations": [
{
"attestationUid": "0x…",
"schemaUid": "0xb076…554e",
"issuerEntityType": "service-provider",
"issuerEntityId": "stord-prod",
"issuerWallet": "0x…",
"subjectEntityType": "merchant",
"subjectEntityId": "6a0001a08692425bd9fd571b",
"trustScore": 88,
"basis": "12-month custody relationship, 0 inventory disputes, $4.2M of throughput",
"attestedAt": "2026-06-14T12:00:00Z",
"status": "ACTIVE",
"attestorCurrentStatus": "ACTIVE",
"attestorCurrentStatusAt": "2026-06-14T18:00:00Z"
}
]
}
```
## Read via viem (TypeScript)
```ts theme={null}
import { createPublicClient, http, parseAbi, decodeAbiParameters } from "viem";
import { base } from "viem/chains";
const EAS = "0x4200000000000000000000000000000000000021" as const;
const SCHEMA_D = "0xb076cf65e9547678fe0e0c5ff010b504e7133ffe03977dc5fad2598723e5554e" as const;
const client = createPublicClient({ chain: base, transport: http() });
const easAbi = parseAbi([
"function getAttestation(bytes32 uid) view returns (tuple(bytes32 uid, bytes32 schema, uint64 time, uint64 expirationTime, uint64 revocationTime, bytes32 refUID, address recipient, address attester, bool revocable, bytes data))",
]);
const uid = "0x…"; // UID from /v2/attestations/cross/*
const att = await client.readContract({
address: EAS,
abi: easAbi,
functionName: "getAttestation",
args: [uid],
});
if (att.schema !== SCHEMA_D) throw new Error("not a Schema D attestation");
const [issuerRootUid, subjectRootUid, trustScore, basis, attestedAt] =
decodeAbiParameters(
[
{ name: "issuerRootUid", type: "string" },
{ name: "subjectRootUid", type: "string" },
{ name: "trustScore", type: "uint8" },
{ name: "basis", type: "string" },
{ name: "attestedAt", type: "uint256" },
],
att.data,
);
const isActive = att.revocationTime === 0n;
console.log({ issuerRootUid, subjectRootUid, trustScore: Number(trustScore), basis, attestedAt, isActive });
```
## Read via ethers (TypeScript)
```ts theme={null}
import { JsonRpcProvider, Contract, AbiCoder } from "ethers";
const EAS = "0x4200000000000000000000000000000000000021";
const SCHEMA_D = "0xb076cf65e9547678fe0e0c5ff010b504e7133ffe03977dc5fad2598723e5554e";
const provider = new JsonRpcProvider("https://mainnet.base.org");
const eas = new Contract(
EAS,
[
"function getAttestation(bytes32 uid) view returns (tuple(bytes32 uid, bytes32 schema, uint64 time, uint64 expirationTime, uint64 revocationTime, bytes32 refUID, address recipient, address attester, bool revocable, bytes data))",
],
provider,
);
const uid = "0x…";
const att = await eas.getAttestation(uid);
if (att.schema.toLowerCase() !== SCHEMA_D) throw new Error("schema mismatch");
const [issuerRootUid, subjectRootUid, trustScore, basis, attestedAt] =
AbiCoder.defaultAbiCoder().decode(
["string", "string", "uint8", "string", "uint256"],
att.data,
);
console.log({ issuerRootUid, subjectRootUid, trustScore, basis, attestedAt });
```
## Read via Cast (Foundry)
```bash theme={null}
cast call 0x4200000000000000000000000000000000000021 \
"getAttestation(bytes32)((bytes32,bytes32,uint64,uint64,uint64,bytes32,address,address,bool,bytes))" \
0x…UID… \
--rpc-url https://mainnet.base.org
cast abi-decode \
"x(string,string,uint8,string,uint256)" \
0x…data-field…
```
## Indexer access (EAS GraphQL)
* Base mainnet: [https://base.easscan.org/graphql](https://base.easscan.org/graphql)
* Base Sepolia: [https://base-sepolia.easscan.org/graphql](https://base-sepolia.easscan.org/graphql)
**"All Schema D attestations issued BY this wallet"** — issuer-side enumeration:
```graphql theme={null}
query IssuedBy($wallet: String!) {
attestations(
where: {
schemaId: { equals: "0xb076cf65e9547678fe0e0c5ff010b504e7133ffe03977dc5fad2598723e5554e" }
attester: { equals: $wallet }
}
orderBy: { time: desc }
take: 100
) {
id
time
revocationTime
recipient
data
}
}
```
**"All Schema D attestations targeting this subject"** — for fast subject-side enumeration with already-decoded fields, use the Droplinked API (`GET /v2/attestations/cross/subject/:rootUid`). EAS GraphQL has no native index on the encoded `subjectRootUid` string.
## Trust assumptions
A Schema D attestation is authoritative if and only if:
1. The on-chain `schema` matches the mainnet UID above.
2. The on-chain `attester` wallet maps to a currently `ACTIVE` registered entity. For `service-provider` issuers, check `ServiceProviderRegistry` (Droplinked-side admin lookup) — the verifier API exposes the mirror via `attestorCurrentStatus`.
3. `revocationTime == 0`.
4. The semantic interpretation of `trustScore` is the consumer's responsibility — Droplinked does **not** canonicalize a comparable scale across `issuerEntityType` classes. Always read `basis` and `issuerEntityType` together with the score.
For non-`service-provider` issuer classes (merchant, lender, business-buyer), the reconciler does not yet wire registry status into `attestorCurrentStatus`; it will return `null`. Cross-check the issuer directly against [`/v2/lenders/:lenderId`](/api-reference/public/lender-registry) (for lender issuers) or the merchant verifier path until those mirrors land.
## Related
* [Trust Fabric overview](/concepts/trust-fabric)
* [Schemas overview](/api-reference/public/eas-schemas/overview)
* [Schema B — Credit-risk attestation](/api-reference/public/eas-schemas/schema-b-writer) (the most-disputed surface)
* [Schema C — Repayment-history attestation](/api-reference/public/eas-schemas/schema-c-reader)
* [`get_trust_dossier` MCP tool](/agentic/mcp-server) — multi-axis composite read
# Lender Registry Lookup
Source: https://docs.droplinked.com/api-reference/public/lender-registry
Resolve a `lenderId` referenced in a Schema B credit-risk attestation to the lender's public profile + signing wallet (forensic cross-check vs the on-chain `issuerWallet`).
`GET /v2/lenders/:lenderId` returns the public profile of a registered lender — used by verifiers (consumer agents, MCP `verify_credit_risk` callers, partner portals) to resolve the `lenderId` embedded in a Schema B credit-risk attestation to a human-readable lender + their on-chain signing wallet for forensic cross-check.
This endpoint is **public** — no JWT, no IP-allowlist. Only ACTIVE / PENDING\_KYB / SUSPENDED / ARCHIVED lifecycle states are exposed (no operator-only fields).
## When to use
* A consumer agent has parsed a Schema B attestation and needs to display **who** underwrote the merchant (human-readable name + jurisdiction + regulatory reference)
* A verifier needs to **cross-check** the on-chain `issuerWallet` against the registered `signingWallet` to detect schema impersonation
* A partner portal needs to show track-record (`issuedAttestationCount`, `lastAttestationAt`) before routing a merchant to this lender
## Request
```
GET /v2/lenders/:lenderId
```
| Param | Type | Notes |
| ---------- | ------ | ---------------------------------------------------- |
| `lenderId` | string | Kebab-case, 3-64 chars, matches `^[a-z0-9_-]{3,64}$` |
## Response (200, found)
```json theme={null}
{
"found": true,
"lenderId": "crediblex-uae",
"displayName": "CredibleX (UAE)",
"archetype": "fsra-licensed",
"jurisdiction": "AE",
"status": "ACTIVE",
"signingWallet": "0xD5F6FB7b6E71DD7F609b8442951444E5a5C76cce",
"regulatorReference": "FSRA-12345",
"issuedAttestationCount": 42,
"lastAttestationAt": "2026-06-11T22:00:00Z"
}
```
## Response (200, not found)
```json theme={null}
{
"found": false,
"lenderId": "unknown-lender"
}
```
The `found` discriminator lets MCP / agent consumers branch without 404-handling.
## Field reference
| Field | Type | Notes |
| ------------------------ | -------------- | ----------------------------------------------------------------------------------------------------------- |
| `displayName` | string | Human-readable lender name (operator-set) |
| `archetype` | enum | `fsra-licensed` \| `defi-vault` \| `generic` |
| `jurisdiction` | string | ISO 3166-1 alpha-2 country code, or `GLOBAL` for unrestricted (DeFi vaults) |
| `status` | enum | `PENDING_KYB` \| `ACTIVE` \| `SUSPENDED` \| `ARCHIVED` — only ACTIVE lenders can mint Schema B attestations |
| `signingWallet` | hex | 0x-prefixed 20-byte EVM address used to sign attestations on-chain |
| `regulatorReference` | string \| null | FSRA / SCA / etc. license number when applicable |
| `issuedAttestationCount` | int | Lifetime count of Schema B attestations this lender has issued |
## Lifecycle timeline
```
GET /v2/lenders/:lenderId/timeline
```
Returns the whitelist-redacted lifecycle history of a registered lender — used by third-party verifiers asking the temporal question: **was this lender `ACTIVE` at the time a Schema B attestation was minted?** A point-in-time `status` lookup against `GET /v2/lenders/:lenderId` only answers *right now*; the timeline lets a verifier replay the lender's state at any `occurredAt` in the past.
This endpoint is **public** — no JWT, no IP-allowlist. The response is intentionally redacted to a verifier-safe whitelist. **The following operator-only fields are deliberately NOT in the response:** `actorId` (which admin made the change), `reason` (free-text justification), and raw before/after value diffs for metadata-change events (display name, jurisdiction, signing wallet, regulator reference, contact notes). Only `previousStatus` / `newStatus` are exposed, and only for `LENDER_REGISTERED` + `LENDER_STATUS_CHANGED` events.
### Request
| Param | Type | Notes |
| ---------- | ------ | ---------------------------------------- |
| `lenderId` | string | Kebab-case, matches `^[a-z0-9_-]{3,64}$` |
### Response (200)
```json theme={null}
{
"lenderId": "crediblex-uae",
"count": 3,
"events": [
{
"occurredAt": "2026-06-10T14:22:11Z",
"eventType": "LENDER_STATUS_CHANGED",
"previousStatus": "PENDING_KYB",
"newStatus": "ACTIVE"
},
{
"occurredAt": "2026-06-08T09:15:03Z",
"eventType": "LENDER_DISPLAY_NAME_CHANGED",
"previousStatus": null,
"newStatus": null
},
{
"occurredAt": "2026-06-01T00:00:00Z",
"eventType": "LENDER_REGISTERED",
"previousStatus": null,
"newStatus": "PENDING_KYB"
}
]
}
```
Events are ordered **newest-first** and capped at **500 events** per response (hard cap — older events are not paginated).
### Event types
| `eventType` | Carries `previousStatus` / `newStatus`? |
| ------------------------------------ | ----------------------------------------------------------------- |
| `LENDER_REGISTERED` | `previousStatus: null`, `newStatus` set (typically `PENDING_KYB`) |
| `LENDER_STATUS_CHANGED` | both set |
| `LENDER_DISPLAY_NAME_CHANGED` | both `null` |
| `LENDER_JURISDICTION_CHANGED` | both `null` |
| `LENDER_SIGNING_WALLET_CHANGED` | both `null` |
| `LENDER_REGULATOR_REFERENCE_CHANGED` | both `null` |
| `LENDER_CONTACT_NOTES_CHANGED` | both `null` |
`previousStatus` / `newStatus` values are drawn from the lifecycle enum: `PENDING_KYB` | `ACTIVE` | `SUSPENDED` | `ARCHIVED`. For metadata-change events the raw before/after values are intentionally redacted — verifiers can confirm *that* a change occurred at `occurredAt` (and combine that with the current public profile) but cannot read the operator-only diff.
### Curl example
```bash theme={null}
curl -s https://apiv3.droplinked.com/v2/lenders/crediblex-uae/timeline | jq .
```
### MCP wrapper
Consumer agents reach this endpoint via the [`get_lender_history`](/agentic/lender-trinity-mcp-tools) MCP tool — already live on [`mcp.droplinked.com`](https://mcp.droplinked.com) — so ChatGPT / Claude / OpenAI Agents SDK callers can replay a lender's lifecycle without prior knowledge of the path scheme.
## Related
* [Lender Routing Recommendation](/api-reference/public/lender-routing) — "which lender should this merchant approach"
* [Schema B Credit-Risk Read](/api-reference/public/attestation-credit-risk) — verify an attestation issued by this lender
# Lender Routing Recommendation
Source: https://docs.droplinked.com/api-reference/public/lender-routing
Given a merchant's jurisdiction, returns the ordered list of ACTIVE lenders most-likely to underwrite. Exact-jurisdiction first, GLOBAL fallback second, track-record sort within group.
`GET /v2/lender-routing/recommend` returns the ordered list of ACTIVE lenders most-likely to underwrite a merchant in the given jurisdiction. Used by partner portals, merchant onboarding flows, and MCP agent flows resolving "which lender should this merchant approach."
This endpoint is **public** and **read-only**. It only returns ACTIVE lenders. Calling this does not initiate or imply any financing application — the actual application flow stays gated on `LenderRegistry.isActive` at mint time.
## Query parameters
| Param | Type | Default | Notes |
| -------------- | ------ | -------- | ---------------------------------------------------------------------- |
| `jurisdiction` | string | `GLOBAL` | ISO 3166-1 alpha-2 (e.g. `AE`, `US`) or `GLOBAL` |
| `archetype` | enum | (all) | `fsra-licensed` \| `defi-vault` \| `generic` — filter to one archetype |
| `limit` | int | `10` | Clamped to `[1, 50]` |
## Ranking
1. **Exact-jurisdiction matches** ranked first
2. **`GLOBAL` fallback lenders** ranked after exact matches
3. Within each group, sorted by `issuedAttestationCount` desc (proven track record first)
## Example
```bash theme={null}
curl 'https://apiv3.droplinked.com/v2/lender-routing/recommend?jurisdiction=AE&archetype=fsra-licensed&limit=5'
```
```json theme={null}
{
"jurisdiction": "AE",
"archetype": "fsra-licensed",
"count": 2,
"recommendations": [
{
"lenderId": "crediblex-uae",
"displayName": "CredibleX (UAE)",
"archetype": "fsra-licensed",
"jurisdiction": "AE",
"matchKind": "exact-jurisdiction",
"rank": 1
},
{
"lenderId": "valinor-vault",
"displayName": "Valinor Vault",
"archetype": "defi-vault",
"jurisdiction": "GLOBAL",
"matchKind": "global-fallback",
"rank": 2
}
]
}
```
## When `matchKind` matters
A verifier or merchant portal may surface lenders differently based on `matchKind`:
| matchKind | Suggested UI framing |
| -------------------- | ----------------------------- |
| `exact-jurisdiction` | "Recommended for your region" |
| `global-fallback` | "Available globally" |
The ranking guarantees that an exact match is always offered before a fallback — but the verifier decides what to render.
## Related
* [Lender Registry Lookup](/api-reference/public/lender-registry) — resolve a `lenderId` to a full profile
* [Service-Provider Routing](/api-reference/public/service-provider-routing) — same pattern for WMS/3PL partners
* MCP tool: `recommend_lender` (wraps this endpoint)
# Methodology Registry Lookup
Source: https://docs.droplinked.com/api-reference/public/methodology-registry
Resolve the `methodologyHash` pinned to a Schema B credit-risk attestation to its source document — third-party verifier flow.
`GET /v2/methodologies/:lenderId/active` and `GET /v2/methodologies/:lenderId/:methodologyHash` resolve the per-lender underwriting methodology document linked from a Schema B attestation. Used by regulators and third-party verifiers to independently audit a lender's underwriting basis.
These endpoints are **public** — no JWT, no IP-allowlist. They return the `documentUrl` so a verifier can download the source PDF/markdown and re-hash it locally; any divergence flags a compromised methodology.
## Why this exists
A Schema B credit-risk attestation pins `methodologyHash` so a verifier can cryptographically prove which underwriting scorecard a lender used at attestation time. The hash alone is opaque — these endpoints resolve it back to the source document + version + lifecycle status, completing the verifier flow.
Combined with the [audit-trail trinity](/concepts/trust-fabric), this means:
1. **Schema B attestation** carries `methodologyHash` (on-chain, immutable)
2. **`GET /v2/methodologies/:lenderId/:hash`** resolves it to `documentUrl` + `version` + lifecycle status
3. **`SUPERSEDED` / `REVOKED`** status indicates the methodology is no longer in force — verifier-side policy decides whether to honor historical attestations
4. **Operator-side audit log** (SUPER\_ADMIN) preserves the full lifecycle for regulator review
## Endpoint 1 — current ACTIVE methodology
```
GET /v2/methodologies/:lenderId/active
```
Returns the methodology document currently in force for new Schema B mints by this lender. Returns `{ found: false }` when the lender has never registered a methodology (callers should fall back to the v0 well-known constant).
### Request
| Param | Type | Notes |
| ---------- | ------ | ---------------------------------------- |
| `lenderId` | string | Kebab-case, matches `^[a-z0-9_-]{3,64}$` |
### Response (200, found)
```json theme={null}
{
"found": true,
"lenderId": "crediblex-uae",
"version": "2.1.0",
"methodologyHash": "1f8b3c…",
"documentUrl": "https://crediblex.example.com/methodology/v2.1.0.pdf",
"displayName": "CredibleX UAE — SME Working-Capital Methodology v2.1",
"status": "ACTIVE",
"effectiveAt": "2026-05-01T00:00:00Z"
}
```
### Response (200, not registered)
```json theme={null}
{ "found": false, "lenderId": "some-lender" }
```
## Endpoint 2 — lookup by hash (verifier flow)
```
GET /v2/methodologies/:lenderId/:methodologyHash
```
The canonical verifier path. Given a `lenderId` + `methodologyHash` parsed from an on-chain Schema B attestation, resolve it back to the source document.
### Request
| Param | Type | Notes |
| ----------------- | ------ | ------------------------------------------------------ |
| `lenderId` | string | Kebab-case |
| `methodologyHash` | string | 64-char lowercase hex (SHA-256 of the source document) |
### Response (200, found)
```json theme={null}
{
"found": true,
"lenderId": "crediblex-uae",
"version": "1.0.0",
"methodologyHash": "a91d…",
"documentUrl": "https://crediblex.example.com/methodology/v1.0.0.pdf",
"displayName": "CredibleX UAE — SME Working-Capital Methodology v1.0",
"status": "SUPERSEDED",
"effectiveAt": "2026-02-01T00:00:00Z",
"supersededAt": "2026-05-01T00:00:00Z"
}
```
`status` values: `ACTIVE` | `SUPERSEDED` | `REVOKED`. A verifier seeing `SUPERSEDED` on an attestation issued **before** `supersededAt` should still honor it (the methodology was in force at mint time); seeing `REVOKED` is a red flag and verifier-side policy decides whether to honor.
### Response (200, not found)
```json theme={null}
{
"found": false,
"lenderId": "crediblex-uae",
"methodologyHash": "00…"
}
```
## List all versions for a lender
```
GET /v2/methodologies/:lenderId/versions
```
Returns the **full versioned lineage** of methodology documents this lender has ever registered — ACTIVE, SUPERSEDED, and REVOKED — sorted newest-first by `effectiveAt`. Verifiers use this when they want to walk a lender's methodology history without already knowing specific `methodologyHash` values upfront (e.g. "show me every version CredibleX has ever published" for a regulator audit).
This endpoint is **public** — no JWT, no IP-allowlist. The response is intentionally redacted to a verifier-safe whitelist. **Operator-only fields are deliberately NOT in the response:** `notes` (operator-only free-text), `createdBy`, `updatedBy`, and any internal lifecycle metadata. `lenderId` appears once in the envelope and is omitted from each row to avoid redundant payload.
### Request
| Param | Type | Notes |
| ---------- | ------ | ---------------------------------------- |
| `lenderId` | string | Kebab-case, matches `^[a-z0-9_-]{3,64}$` |
No query parameters. The response is capped at **100 versions** (hard cap — older versions are not paginated; a lender publishing >100 methodology versions is a registry-design escalation, not a paging escalation).
### Response (200)
```json theme={null}
{
"lenderId": "crediblex-uae",
"count": 3,
"versions": [
{
"version": "2.1.0",
"methodologyHash": "1f8b3c…",
"documentUrl": "https://crediblex.example.com/methodology/v2.1.0.pdf",
"displayName": "CredibleX UAE — SME Working-Capital Methodology v2.1",
"status": "ACTIVE",
"effectiveAt": "2026-05-01T00:00:00Z",
"supersededAt": null
},
{
"version": "2.0.0",
"methodologyHash": "b2e4f1…",
"documentUrl": "https://crediblex.example.com/methodology/v2.0.0.pdf",
"displayName": "CredibleX UAE — SME Working-Capital Methodology v2.0",
"status": "SUPERSEDED",
"effectiveAt": "2026-03-15T00:00:00Z",
"supersededAt": "2026-05-01T00:00:00Z"
},
{
"version": "1.0.0",
"methodologyHash": "a91d…",
"documentUrl": "https://crediblex.example.com/methodology/v1.0.0.pdf",
"displayName": "CredibleX UAE — SME Working-Capital Methodology v1.0",
"status": "SUPERSEDED",
"effectiveAt": "2026-02-01T00:00:00Z",
"supersededAt": "2026-03-15T00:00:00Z"
}
]
}
```
### Field reference
| Field | Type | Notes |
| ---------------------------- | ---------------- | ----------------------------------------------------------------------- |
| `lenderId` | string | Envelope only; not duplicated per row |
| `count` | integer | Total versions returned (≤ 100 hard cap) |
| `versions[].version` | string | Lender-published semantic version (e.g. `2.1.0`) |
| `versions[].methodologyHash` | string | 64-char lowercase hex (SHA-256 of source document) |
| `versions[].documentUrl` | string | URL of the source PDF/markdown — verifiers re-hash to confirm integrity |
| `versions[].displayName` | string | Human-readable label |
| `versions[].status` | enum | `ACTIVE` \| `SUPERSEDED` \| `REVOKED` |
| `versions[].effectiveAt` | ISO 8601 | When this version went into force |
| `versions[].supersededAt` | ISO 8601 \| null | When this version was superseded; `null` for the current `ACTIVE` row |
### Sort order
Versions are returned **newest-first by `effectiveAt`** — the current `ACTIVE` row (if any) is row 0; the lender's earliest registration is the last row.
### Curl example
```bash theme={null}
curl -s https://apiv3.droplinked.com/v2/methodologies/crediblex-uae/versions | jq .
```
### When to use this vs. the by-hash / active endpoints
* Use [`GET /v2/methodologies/:lenderId/active`](/api-reference/public/methodology-registry) (Endpoint 1 above) when you already know the lender and just want the *current* in-force methodology.
* Use [`GET /v2/methodologies/:lenderId/:methodologyHash`](/api-reference/public/methodology-registry) (Endpoint 2 above) when you have a specific `methodologyHash` from a Schema B attestation and want to resolve it to its source document.
* Use this `versions` endpoint when you want the **full lineage** without a specific hash in hand — for example, regulator audits asking "show me every methodology this lender has ever issued attestations against."
### MCP wrapper
Consumer agents reach this endpoint via the `get_methodology_versions` MCP tool (in flight as a separate droplinked-mcp PR — will be listed on the [Lender Trinity MCP Tools](/agentic/lender-trinity-mcp-tools) page once shipped) so ChatGPT / Claude / OpenAI Agents SDK callers can walk a lender's methodology lineage without prior knowledge of the path scheme.
## Verifier integrity check
A complete verifier flow:
```bash theme={null}
# 1. Read the on-chain attestation
LENDER=$(curl -s https://apiv3.droplinked.com/v2/attestations/credit-risk/$MERCHANT \
| jq -r '.attestation.lenderId')
HASH=$(curl -s https://apiv3.droplinked.com/v2/attestations/credit-risk/$MERCHANT \
| jq -r '.attestation.methodologyHash')
# 2. Resolve methodology + download source
DOC_URL=$(curl -s https://apiv3.droplinked.com/v2/methodologies/$LENDER/$HASH \
| jq -r '.documentUrl')
curl -sLo /tmp/methodology.pdf "$DOC_URL"
# 3. Re-hash locally and compare
LOCAL=$(shasum -a 256 /tmp/methodology.pdf | awk '{print $1}')
[ "$LOCAL" = "$HASH" ] && echo "verified" || echo "TAMPERED"
```
If the local hash diverges from the on-chain hash, the methodology document at `documentUrl` was modified after the attestation was issued — flag and refuse.
## Cross-reference
* [Trust Fabric overview](/concepts/trust-fabric) — where MethodologyRegistry fits in the 4-axis trust fabric
* [Forensic chain workflow](/concepts/forensic-chain) — end-to-end verifier walkthrough
* [Lender Registry Lookup](/api-reference/public/lender-registry) — resolve the `lenderId` to its public profile
* [MCP `verify_methodology` tool](/agentic/lender-trinity-mcp-tools) — agent-runtime wrapper
## Discovery
The MCP server advertises this endpoint via the discovery doc at [`mcp.droplinked.com/.well-known/mcp.json`](https://mcp.droplinked.com/.well-known/mcp.json) under the `verify_methodology` tool, so consumer agents (ChatGPT, Claude, OpenAI Agents SDK) reach it without any prior knowledge of the path scheme.
## Lifecycle timeline
```
GET /v2/methodologies/:lenderId/:methodologyHash/timeline
```
Returns the whitelist-redacted lifecycle history of a single methodology version (keyed by `lenderId` + `methodologyHash`) — used by third-party verifiers asking the temporal question: **was this methodology version `ACTIVE` at the time a Schema B attestation was minted, or was it already `SUPERSEDED` / `REVOKED`?** The point-in-time `status` from `GET /v2/methodologies/:lenderId/:methodologyHash` only answers *right now*; the timeline lets a verifier replay state at the attestation's `occurredAt`.
This endpoint is **public** — no JWT, no IP-allowlist. The response is intentionally redacted to a verifier-safe whitelist. **The following operator-only fields are deliberately NOT in the response:** `actorId` (which admin made the change), `reason` (free-text justification), and raw before/after value diffs for metadata-change events (display name, document URL, notes, semantic version). Only `previousStatus` / `newStatus` are exposed, and only for `METHODOLOGY_REGISTERED` + `METHODOLOGY_SUPERSEDED` + `METHODOLOGY_REVOKED` events.
### Request
| Param | Type | Notes |
| ----------------- | ------ | ------------------------------------------------------ |
| `lenderId` | string | Kebab-case, matches `^[a-z0-9_-]{3,64}$` |
| `methodologyHash` | string | 64-char lowercase hex (SHA-256 of the source document) |
### Response (200)
```json theme={null}
{
"lenderId": "crediblex-uae",
"methodologyHash": "a91d…",
"count": 3,
"events": [
{
"occurredAt": "2026-05-01T00:00:00Z",
"eventType": "METHODOLOGY_SUPERSEDED",
"previousStatus": "ACTIVE",
"newStatus": "SUPERSEDED"
},
{
"occurredAt": "2026-03-15T10:30:00Z",
"eventType": "METHODOLOGY_DISPLAY_NAME_CHANGED",
"previousStatus": null,
"newStatus": null
},
{
"occurredAt": "2026-02-01T00:00:00Z",
"eventType": "METHODOLOGY_REGISTERED",
"previousStatus": null,
"newStatus": "ACTIVE"
}
]
}
```
Events are ordered **newest-first** and capped at **500 events** per response (hard cap — older events are not paginated).
### Event types
| `eventType` | Carries `previousStatus` / `newStatus`? |
| ---------------------------------- | --------------------------------------------------------- |
| `METHODOLOGY_REGISTERED` | `previousStatus: null`, `newStatus: "ACTIVE"` |
| `METHODOLOGY_SUPERSEDED` | both set (`ACTIVE` → `SUPERSEDED`) |
| `METHODOLOGY_REVOKED` | both set (typically `ACTIVE` or `SUPERSEDED` → `REVOKED`) |
| `METHODOLOGY_DISPLAY_NAME_CHANGED` | both `null` |
| `METHODOLOGY_DOCUMENT_URL_CHANGED` | both `null` |
| `METHODOLOGY_NOTES_CHANGED` | both `null` |
| `METHODOLOGY_VERSION_CHANGED` | both `null` |
`previousStatus` / `newStatus` values are drawn from the lifecycle enum: `ACTIVE` | `SUPERSEDED` | `REVOKED`. For metadata-change events the raw before/after values are intentionally redacted — verifiers can confirm *that* a change occurred at `occurredAt` but cannot read the operator-only diff (use the source-document re-hash flow from [Verifier integrity check](#verifier-integrity-check) above to detect document-content tampering).
### Verifier policy
The point of the timeline is to let a verifier decide whether a methodology was in force at the moment the on-chain attestation was minted:
* **`SUPERSEDED` at check time, but `ACTIVE` at the attestation's `occurredAt`** → **legitimate**. The methodology was the lender's current scorecard when they signed; the supersession is a forward-looking change. The lifecycle is monotonic in the verifier-friendly direction: `ACTIVE` events strictly precede `SUPERSEDED` events for the same version.
* **`REVOKED` at any point** → **red flag**. Revocation indicates the methodology was withdrawn (e.g. flawed risk model, compliance issue). Verifier-side policy decides whether to honor historical attestations against a revoked methodology; the default agent posture is to flag and surface for human review.
### Curl example
```bash theme={null}
curl -s https://apiv3.droplinked.com/v2/methodologies/crediblex-uae/a91d…/timeline | jq .
```
### MCP wrapper
Consumer agents reach this endpoint via the [`get_methodology_timeline`](/agentic/lender-trinity-mcp-tools) MCP tool — already live on [`mcp.droplinked.com`](https://mcp.droplinked.com) — so ChatGPT / Claude / OpenAI Agents SDK callers can replay a methodology's lifecycle without prior knowledge of the path scheme.
# Billing Invoices (Merchant)
Source: https://docs.droplinked.com/api-reference/public/monetization/billing-invoices
Trailing 1-year billing history for the authenticated merchant. Paid subscription invoices + platform fees retained. Fail-open semantics.
`GET /v2/merchant/billing/invoices` returns the authenticated merchant's billing
history for the **trailing 365 days**: per-invoice rows plus a totals envelope
(paid amount, platform fees retained, invoice count). This endpoint is the
data source for the merchant **Billing History** page in the dashboard.
This endpoint requires:
* **Merchant JWT** — any authenticated merchant role (`OWNER`, `MEMBER`, `PRODUCER`)
The merchant scope is **derived from the JWT `sub` claim**. There is no
`merchantId` query parameter. If a client passes one anyway it is silently
ignored — the response is always scoped to the JWT-bound merchant.
## GET /v2/merchant/billing/invoices
### Authentication
| Guard | Requirement |
| ----- | --------------------------------------------------------------- |
| JWT | Required, any authenticated merchant role |
| Scope | `merchantId` derived from JWT `sub` — **not** from query / body |
Obtain a merchant JWT via `POST /merchant/admin/login` — see
[Authentication](/authentication). SUPER\_ADMIN tokens work too (they are merchant
JWTs with an extra role); the response is scoped to the SUPER\_ADMIN's own
merchant record, not to the platform.
### Query parameters
None. The window is fixed at trailing 365 days.
### Example
```bash theme={null}
curl "https://apiv3.droplinked.com/v2/merchant/billing/invoices" \
-H "Authorization: Bearer "
```
### Response — 200 OK
```json theme={null}
{
"windowStart": "2025-06-14T23:08:50.155Z",
"windowEnd": "2026-06-14T23:08:50.155Z",
"totals": {
"paidUsd": 0,
"platformFeesRetainedUsd": 0,
"invoiceCount": 0
},
"invoices": []
}
```
### Fields
| Field | Type | Nullable | Description |
| -------------------------------- | --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `windowStart` | ISO-8601 string | No | Start of the rolling window (`windowEnd − 365 days`, server clock, UTC). |
| `windowEnd` | ISO-8601 string | No | End of the rolling window (request timestamp, server clock, UTC). |
| `totals` | object | No | Window-aggregate envelope. |
| `totals.paidUsd` | number | No | Sum of paid invoice amounts in the window. **USD major units** (dollars, not cents). |
| `totals.platformFeesRetainedUsd` | number | No | Sum of platform-fee portion retained by Droplinked across the window. **USD major units**. |
| `totals.invoiceCount` | integer | No | Count of `invoices[]` returned in the window. |
| `invoices` | array | No | One entry per paid invoice in the window, newest first. Empty array for merchants with no paid invoices in the trailing 365d. |
| `invoices[].invoiceId` | string | No | Invoice store `_id`. |
| `invoices[].issuedAt` | ISO-8601 string | No | Invoice issuance timestamp (UTC). |
| `invoices[].paidAt` | ISO-8601 string | Yes | Payment-settled timestamp. `null` for unpaid rows (only `PAID` invoices are returned today). |
| `invoices[].paidUsd` | number | No | Invoice amount paid, USD major units. |
| `invoices[].platformFeeUsd` | number | No | Platform-fee portion of `paidUsd` retained by Droplinked. |
| `invoices[].planId` | string | Yes | Subscription plan id at the time of the invoice. `null` for one-off charges. |
| `invoices[].invoicePdfUrl` | string | Yes | Signed CloudFront URL for the rendered PDF. `null` for legacy rows that pre-date PDF rendering. |
### Errors
| Status | Body | When |
| ------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `401` | `{ "statusCode": 401, "status": "failed", "data": { "message": "Unauthorized" } }` | Missing or invalid JWT |
| `5xx` | `{ "statusCode": 500, "status": "failed", ... }` | Hard backend failure — see Notes below for the fail-open contract |
### Notes
* **Trailing 365-day window.** `windowStart` is computed as `now − 365d` on every
call. The window is rolling, not anchored — two calls a day apart return
slightly different `windowStart` / `windowEnd` values, and an invoice issued
exactly 365d ago may drop out of one call's window and back into the next.
* **Auth scope.** The endpoint reads `merchantId` from the JWT `sub` claim
only. Any `?merchantId=` (or body `merchantId`) is silently ignored. A
merchant cannot read another merchant's invoices through this endpoint —
even with a syntactically valid query parameter pointing at the target.
* **Fail-open semantics.** If the underlying invoice store throws, the
endpoint returns the empty-state envelope (`invoices: []`, `totals.*: 0`)
with `200 OK` rather than propagating the error. Inspect server logs /
Sentry for the underlying failure. Merchants never see a partial
billing-history page error in the dashboard.
* **Currency.** All monetary fields are **USD major units** (dollars), not
cents. Renders straight into
`Intl.NumberFormat({ style: 'currency', currency: 'USD' })` without
dividing by 100. This contract diverges from the
[admin x402-earnings rollup](/api-reference/admin/monetization/x402-earnings)
(USD cents, integer) intentionally — merchant-facing endpoints serve
pre-formatted human values; admin rollups serve aggregation-safe integer
cents.
* **Only PAID invoices.** Today the endpoint returns rows where
`status === 'PAID'`. Pending / void / refunded invoices are out of scope
for v1 and tracked for a future revision.
## Related
* [x402 Earnings (Merchant)](/api-reference/public/monetization/x402-earnings) — companion merchant-facing rollup of x402 settlement events.
* [Platform Fee Summary (Admin)](/api-reference/admin/monetization/platform-fee-summary) — network-wide MRR / ARR / 30d / 365d rollup that aggregates the per-merchant billing surfaced here.
* [x402 Earnings (Admin)](/api-reference/admin/monetization/x402-earnings) — admin-side per-merchant x402 settlement rollup.
# Merchant Orders + Attestation JOIN (Merchant)
Source: https://docs.droplinked.com/api-reference/public/monetization/orders
Per-merchant OrderV2 rows JOINed with Schema C repayment_history_attestations + server-computed KPI envelope. Pillar 2 Trust Fabric × Pillar 4 MCP dual-purpose. Per-section fail-open.
`GET /v2/merchant/orders` returns the authenticated merchant's recent OrderV2
rows, JOINed against the **Schema C** `repayment_history_attestations`
collection for each row's attestation block, plus a server-computed KPI
envelope and pagination. This endpoint is the data source for the merchant
**Orders V2** page (Designer spec § 9) and the corresponding MCP
`get_merchant_orders` tool (Pillar 4).
Live on dev as of 2026-06-14; PROD ETA after next GTFU. Shipped via PR
[#2104](https://github.com/droplinked/droplinked-backend/pull/2104). Pillar 2
Trust Fabric (Schema C JOIN) × Pillar 4 MCP (server-computed KPIs) dual-purpose
surface — keep the read path scoped to the merchant rather than forking a
separate MCP-only endpoint.
## GET /v2/merchant/orders
### Authentication
| Guard | Requirement |
| ----- | -------------------------------------------------------------------- |
| JWT | Required, any authenticated merchant role |
| Scope | `shopId` derived from JWT `shopId` claim — **not** from query / body |
Obtain a merchant JWT via `POST /merchant/admin/login` — see
[Authentication](/authentication). Any `?merchantId=` / `?shopId=` query
parameter is silently dropped (OWASP A01:2021 IDOR mitigation; mirrors
`MerchantInventoryAttestationsController` and `MerchantX402EarningsController`).
### Query parameters
| Param | Type | Required | Default | Description |
| ------------ | --------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `page` | integer | No | `1` | 1-indexed page number. |
| `limit` | integer | No | `20` | Rows per page. Capped at `400`. |
| `startDate` | ISO-8601 string | No | — | Filter to orders with `createdAt >= startDate`. Omit for no lower bound. |
| `endDate` | ISO-8601 string | No | — | Filter to orders with `createdAt <= endDate`. Omit for no upper bound. |
| `status` | enum | No | — | FE bucket filter. One of `SETTLED` / `PENDING` / `REFUNDED` / `DISPUTED`. Unknown values silently dropped. See Notes for the 8→4 mapping. |
| `psp` | string | No | — | Filter by PSP provider (e.g. `stripe`, `paypal`, `telr`). Unknown values silently dropped. Applied in-memory post-Prisma. |
| `windowDays` | integer | No | `90` | Trailing window in days when `startDate` / `endDate` are omitted. Clamped to `1..365`. |
### Example
```bash theme={null}
curl "https://apiv3.droplinked.com/v2/merchant/orders?page=1&limit=20" \
-H "Authorization: Bearer "
```
With filters:
```bash theme={null}
curl "https://apiv3.droplinked.com/v2/merchant/orders?page=1&limit=50&status=SETTLED&psp=stripe&startDate=2026-01-01T00:00:00Z&endDate=2026-06-14T23:59:59Z" \
-H "Authorization: Bearer "
```
### Response — 200 OK
```json theme={null}
{
"rows": [
{
"orderId": "6657a1b2c3d4e5f60718293a",
"orderNumber": "ORD-001",
"customerEmail": "buyer@example.com",
"items": 2,
"grossUsdCents": 5000,
"netUsdCents": 4500,
"psp": "stripe",
"status": "SETTLED",
"attestation": {
"uid": "0xUID1",
"status": "MINTED",
"rolledUpAt": "2026-06-13T00:00:00.000Z",
"lenderId": "lender-A",
"chain": "base",
"schemaUid": "0xSCHEMAC"
},
"settledAt": "2026-06-12T00:00:00.000Z",
"createdAt": "2026-06-10T00:00:00.000Z"
}
],
"kpis": {
"totalOrdersInWindow": 4,
"settledRevenueUsdCents": 30000,
"avgOrderValueUsdCents": 15000,
"attestationCoveragePctBps": 2500
},
"pagination": {
"page": 1,
"limit": 20,
"totalPages": 1,
"totalCount": 4
},
"windowDays": 90,
"asOf": "2026-06-14T17:40:00.000Z"
}
```
### Fields
| Field | Type | Nullable | Description |
| -------------------------------- | --------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `rows` | array | No | One entry per OrderV2 row matching the filters. |
| `rows[].orderId` | string | No | OrderV2 `_id`. |
| `rows[].orderNumber` | string | No | Human-readable order number (e.g. `ORD-001`). |
| `rows[].customerEmail` | string | Yes | Buyer email. `null` for guest orders that captured no email. |
| `rows[].items` | integer | No | Total item count across the order's line items. |
| `rows[].grossUsdCents` | integer | No | Gross order amount in **USD cents**, integer. |
| `rows[].netUsdCents` | integer | No | Net to the merchant after platform fees + PSP fees, **USD cents**. |
| `rows[].psp` | string | No | PSP provider that authorized the transaction (e.g. `stripe`, `paypal`, `telr`, `bonum`, `paymob`). |
| `rows[].status` | enum | No | FE bucket: `SETTLED` / `PENDING` / `REFUNDED` / `DISPUTED`. See Notes for the 8→4 collapse. |
| `rows[].attestation` | object | No | Per-row Schema C attestation block. Always present — empty / pending blocks fill the missing-coverage case (see Schema C JOIN section). |
| `rows[].attestation.uid` | string | Yes | EAS attestation UID. `null` when no attestation exists for this order. |
| `rows[].attestation.status` | enum | No | `MINTED` / `PENDING` / `FAILED` / `NOT_APPLICABLE`. |
| `rows[].attestation.rolledUpAt` | ISO-8601 string | Yes | When the attestation was rolled into the on-chain registry. `null` for `PENDING` / `NOT_APPLICABLE`. |
| `rows[].attestation.lenderId` | string | Yes | LenderRegistry id this attestation rolls up to. `null` for `NOT_APPLICABLE`. |
| `rows[].attestation.chain` | string | Yes | Chain the UID is on (`base` for Schema C on Base mainnet). `null` for `NOT_APPLICABLE`. |
| `rows[].attestation.schemaUid` | string | Yes | Schema UID — Schema C (`0x090a...f24c`) for `repayment_history_attestations`. `null` for `NOT_APPLICABLE`. |
| `rows[].settledAt` | ISO-8601 string | Yes | Settlement timestamp (UTC). `null` for non-settled buckets. |
| `rows[].createdAt` | ISO-8601 string | No | Order-creation timestamp (UTC). |
| `kpis` | object | No | Window-aggregate envelope, computed server-side. |
| `kpis.totalOrdersInWindow` | integer | No | Count of orders across the entire window (not just the current page). |
| `kpis.settledRevenueUsdCents` | integer | No | Sum of `grossUsdCents` for SETTLED rows across the entire window. **USD cents**. |
| `kpis.avgOrderValueUsdCents` | integer | No | `settledRevenueUsdCents / count(SETTLED rows)`, integer cents. `0` when no SETTLED rows. |
| `kpis.attestationCoveragePctBps` | integer | No | Schema C attestation coverage as **basis points** (0..10000). `2500` = 25.00%. Integer wire so no float drift. See KPI envelope section. |
| `pagination` | object | No | Pagination envelope. |
| `pagination.page` | integer | No | Echoed `page` query param. |
| `pagination.limit` | integer | No | Effective `limit` (after the 400-cap). |
| `pagination.totalPages` | integer | No | `ceil(totalCount / limit)`. |
| `pagination.totalCount` | integer | No | Total order rows matching the filter (across pages). Equal to `kpis.totalOrdersInWindow`. |
| `windowDays` | integer | No | The effective window size in days (post-clamp). |
| `asOf` | ISO-8601 string | No | Snapshot timestamp (server clock, UTC). |
### Errors
| Status | Body | When |
| ------ | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | `{ "statusCode": 400, "status": "failed", "data": { "message": "startDate must be a valid ISO-8601 date" } }` | `startDate` / `endDate` fail ISO-8601 parsing. Unknown enum values for `status` / `psp` are silently dropped, not rejected. |
| `401` | `{ "statusCode": 401, "status": "failed", "data": { "message": "Unauthorized" } }` | Missing or invalid JWT. |
| `5xx` | (does not occur for read-path failures — see fail-open section) | The endpoint never returns `5xx` for downstream read-path failures; it degrades per-section. Real `5xx` would be a process-level fault. |
### Schema C JOIN
Each `rows[].attestation` block comes from a JOIN against the Schema C
`repayment_history_attestations` collection — the on-chain rollup of order
repayment history that backs the Trust Fabric. Implementation details:
* **Single bulk query.** The JOIN is a single `$in` query against the
sparse `orderId` index on `repayment_history_attestations`. No N+1; the
service collects every `orderId` from the Prisma fetch first, then
issues one Schema C read for the page.
* **Newest-wins.** If multiple Schema C rows reference the same `orderId`
(re-mint flows), the most recently `rolledUpAt` row is returned. The
collection retains history; the JOIN reads the latest.
* **Schema UID.** Every returned block carries the Schema C UID
(`0xSCHEMAC` placeholder above; the production value is registered on
Base mainnet — see the
[EAS Schema v2 shipped 2026-06-11](/concepts/trust-fabric) note in the
trust-fabric concept page). Clients can use this to disambiguate from
Schemas A / B / D in a future mixed-schema view.
* **Missing coverage.** Orders with no Schema C row land
`attestation.status: "NOT_APPLICABLE"` (orders that don't qualify, e.g.
refunded-before-settlement) or `"PENDING"` (qualifying orders not yet
rolled up). Rows are **never** dropped from `rows[]` because of a
missing attestation — coverage is signaled via the per-row `status`
field and aggregated into `kpis.attestationCoveragePctBps`.
### KPI envelope
The KPI envelope is computed **server-side** so the FE never recomputes
totals from the visible page (which would understate the window). All
amounts are USD cents to keep aggregation float-safe; `attestationCoveragePctBps`
is basis points (0..10000) for the same reason — `25.00%` ships as `2500`,
no `0.25` floats on the wire.
* `totalOrdersInWindow` — order rows matching the filter across the full
window (not just the visible page).
* `settledRevenueUsdCents` — `Σ grossUsdCents` for rows whose FE bucket is
`SETTLED`. Refunded / disputed rows are excluded.
* `avgOrderValueUsdCents` — `settledRevenueUsdCents / count(SETTLED rows)`,
integer division. `0` when no SETTLED rows in window.
* `attestationCoveragePctBps` — `floor(10000 × count(attestation.status === "MINTED") / totalOrdersInWindow)`.
Clamped to `0..10000`. `0` when the window is empty.
### Per-section fail-open
The endpoint follows the Stripe Reliability + Shopify Platform fail-open
discipline established by PR #1975 + #2042 — **partial failures degrade
per-section, never propagate as `5xx`**:
| Section | Healthy | Failure mode |
| ------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Prisma OrderV2 read | populated `rows[]` | empty `rows: []`, empty `kpis` envelope, `200 OK`. Sentry-tagged. |
| Schema C JOIN | populated `attestation` blocks | `rows[]` is kept with `attestation.status: "PENDING"` / `"NOT_APPLICABLE"`. Rows are **never** dropped due to JOIN failure — the merchant must still see their own orders. Sentry-tagged. |
| KPI compute | populated `kpis` | rows kept, `kpis` returns the zero-envelope `EMPTY_KPIS` shape (`totalOrdersInWindow: 0`, `settledRevenueUsdCents: 0`, `avgOrderValueUsdCents: 0`, `attestationCoveragePctBps: 0`). Sentry-tagged. |
The merchant never sees a partial-Orders-page error in the dashboard.
### Notes
* **8→4 status mapping.** The Prisma `OrderStatus` enum has 8 values
(`CREATED`, `CONFIRMED`, `PROCESSING`, `SHIPPED`, `DELIVERED`,
`REFUNDED`, `CANCELLED`, `DISPUTED`); the FE bucket has 4
(`SETTLED` / `PENDING` / `REFUNDED` / `DISPUTED`). Mapping is
centralized in `MerchantOrdersService.mapStatusBucket()`:
* `DELIVERED` + `SHIPPED` + `PROCESSING` + `CONFIRMED` → `SETTLED`
* `REFUNDED` + `CANCELLED` → `REFUNDED`
* `DISPUTED` → `DISPUTED`
* `CREATED` and unmapped → `PENDING` (excluded from `SETTLED` revenue;
included in `totalOrdersInWindow`).
* **Caching.** The response carries `Cache-Control: private, max-age=60`.
Clients may see up to a 60s lag for fresh order events through CDN
caches.
* **PSP filter is in-memory.** Prisma's Mongo provider does not
efficiently filter on a composite-type `payment.provider` field, so
the PSP filter is applied in-memory after the Prisma fetch. Pagination
math is over the filtered set.
* **Auth scope.** `shopId` from `req.user.shopId` only. Any
`?merchantId=` / `?shopId=` query parameter is silently dropped. A
merchant cannot read another merchant's orders through this endpoint.
* **Currency.** All monetary fields are **USD cents, integer** — same
contract as
[x402 Earnings (Merchant)](/api-reference/public/monetization/x402-earnings).
Divide by `100` for major-unit display.
* **MCP twin.** The same data feeds the `get_merchant_orders` MCP tool
(Pillar 4). The wire shape is identical so a single OpenAPI schema
generates both clients.
## Related
* [x402 Earnings (Merchant)](/api-reference/public/monetization/x402-earnings) — companion merchant-facing rollup of x402 settlement events.
* [Billing Invoices (Merchant)](/api-reference/public/monetization/billing-invoices) — companion merchant-facing billing history (trailing 365 days).
* [Brand Attestation Lifecycle](/guides/trust-fabric/brand-attestation-lifecycle) — context on the Schema C → repayment-history → LenderRegistry attestation chain.
* [Register Settlement Wallet](/api-reference/public/monetization/wallet-claim) — register where x402 micro-payments derived from these orders should route.
# Register Settlement Wallet (Merchant)
Source: https://docs.droplinked.com/api-reference/public/monetization/wallet-claim
Register the wallet address droplinked should route x402 micro-payment settlements to. Idempotent on (merchantId, network, lowercased wallet). EIP-55 mixed-case dedup. Optional primary-flag semantics.
`POST /v2/merchant/wallet/claim` registers a settlement wallet for the
authenticated merchant. When `X402_ENABLED` flips on at the platform, every
settlement event for the merchant routes to the **primary** wallet on the
event's chain. Without a registered wallet, settlements still land in the
ledger but cannot be paid out.
Live on dev as of 2026-06-14; PROD ETA after next GTFU. Shipped via PR
[#2103](https://github.com/droplinked/droplinked-backend/pull/2103) (Phase 5.4.7).
EIP-191 signed-message ownership verification is deferred to Phase 5.4.8 — for
now the claim records the address and emits an audit event without proving
custody.
## POST /v2/merchant/wallet/claim
### Authentication
| Guard | Requirement |
| ----- | --------------------------------------------------------------- |
| JWT | Required, any authenticated merchant role |
| Scope | `merchantId` derived from JWT `sub` — **not** from query / body |
Obtain a merchant JWT via `POST /merchant/admin/login` — see
[Authentication](/authentication). Any `merchantId` passed in the body is
silently ignored — the response is always scoped to the JWT-bound merchant.
### Request body
| Field | Type | Required | Default | Description |
| --------------- | ------- | -------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `walletAddress` | string | Yes | — | EVM wallet address, `0x` + 40 hex chars. Accepts any case; persisted as the caller submitted, deduped on lowercase. |
| `network` | string | No | `base` | Settlement network. MVP supports `base` only; other values reject with `400`. |
| `label` | string | No | — | Operator-facing label (e.g. `Mainnet Treasury`). Max 64 chars. |
| `isPrimary` | boolean | No | see below | When `true`, marks this wallet as the merchant's primary settlement target on `network`. The prior primary on the same `(merchantId, network)` is cleared in the same write. |
**Default-primary rule.** If `isPrimary` is omitted **and** the merchant has
no prior wallet on this `network`, the new row is persisted as primary.
Single-wallet merchants do not need to set the flag explicitly.
### Example
```bash theme={null}
curl -X POST "https://apiv3.droplinked.com/v2/merchant/wallet/claim" \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"walletAddress": "0xB2721aD4F1c4dD8fE45F3F3c8e4F8c8c5d5f1eA9",
"network": "base",
"label": "Mainnet Treasury",
"isPrimary": true
}'
```
### Response — 200 OK
```json theme={null}
{
"claimed": true,
"wallet": {
"walletAddress": "0xB2721aD4F1c4dD8fE45F3F3c8e4F8c8c5d5f1eA9",
"network": "base",
"label": "Mainnet Treasury",
"isPrimary": true,
"claimedAt": "2026-06-14T17:30:00.000Z",
"lastVerifiedAt": "2026-06-14T17:30:00.000Z"
}
}
```
### Fields
| Field | Type | Nullable | Description |
| ----------------------- | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------ |
| `claimed` | boolean | No | `true` on a successful write. `false` only on the fail-open audit path (see Notes) — the DB row was still written. |
| `wallet` | object | No | The persisted wallet record. |
| `wallet.walletAddress` | string | No | The address as the caller submitted it (case preserved). |
| `wallet.network` | string | No | Echoed network. `base` in MVP. |
| `wallet.label` | string | Yes | Echoed label. `null` when none was supplied. |
| `wallet.isPrimary` | boolean | No | Whether this wallet is the merchant's primary settlement target on `network`. |
| `wallet.claimedAt` | ISO-8601 string | No | Original claim timestamp (UTC). Unchanged across idempotent re-claims. |
| `wallet.lastVerifiedAt` | ISO-8601 string | No | Most-recent claim-call timestamp (UTC). Refreshed on every re-claim. |
### Errors
| Status | Body | When |
| ------ | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | `{ "statusCode": 400, "status": "failed", "data": { "message": "walletAddress must match the EVM 0x+40-hex shape" } }` | `walletAddress` fails the `^0x[a-fA-F0-9]{40}$` shape check, or `network` is anything other than `base`, or `label` exceeds 64 chars. |
| `401` | `{ "statusCode": 401, "status": "failed", "data": { "message": "Unauthorized" } }` | Missing or invalid JWT. |
| `5xx` | `{ "statusCode": 500, "status": "failed", ... }` | DB write failure — see Notes below. |
### Notes
* **Idempotency.** The dedup key is `merchantId:network:walletAddress.toLowerCase()`
and is enforced by a unique index on `merchant_wallet_claims`. Re-claiming
the same address (in any case) updates `lastVerifiedAt` and leaves
`claimedAt` untouched. The response shape is identical between first claim
and re-claim — the caller does not need to branch on a `created` flag.
* **EIP-55 mixed-case dedup.** EVM addresses are canonically case-insensitive
but EIP-55 encodes a checksum in the casing. Two requests with the same
underlying address in different casings (`0xB272…eA9` vs `0xb272…ea9`)
dedupe to a single row. The originally-claimed casing is persisted; the
lowercased form is internal to the dedup index.
* **Primary-flag invariant.** A partial unique index enforces **at most one
primary wallet per `(merchantId, network)`**. The service clears the prior
primary in the same write boundary as the upsert, so the constraint is
satisfied atomically. Demoting a primary requires re-claiming a different
wallet with `isPrimary: true` — there is no direct "demote" RPC.
* **Auth scope.** The endpoint reads `merchantId` from the JWT `sub` claim
only. A merchant cannot register a wallet against another merchant's
scope, even with a `merchantId` field in the body.
* **Fail-open on audit-only.** The DB write is the source of truth. If the
downstream `PlatformAuditEvent` emit fails, the endpoint still returns
`200` with `{ claimed: false, reason: "audit_only" }` — the wallet IS
registered, only the audit-trail side-effect missed. Real DB write
failures propagate as `5xx` so the merchant knows whether their wallet
was registered.
* **No payment-path impact.** This endpoint does not touch
`checkout-intent`, `resolve-payment-intent`, `gateProvider`, or any
settlement-writer surface. PSP backbone is untouched.
## Related
* [List registered wallets](/api-reference/public/monetization/wallet-list) — companion read of every wallet a merchant has registered.
* [Deregister wallet](/api-reference/public/monetization/wallet-deregister) — companion delete, idempotent on the dedup key.
* [x402 Earnings (Merchant)](/api-reference/public/monetization/x402-earnings) — per-merchant rollup of settlement events that route to the primary wallet.
# Deregister Settlement Wallet (Merchant)
Source: https://docs.droplinked.com/api-reference/public/monetization/wallet-deregister
Remove a previously-registered settlement wallet for the authenticated merchant. Idempotent: deregistering a never-claimed wallet returns 200 with deregistered: false.
`DELETE /v2/merchant/wallet/:walletAddress?network=base` removes a previously
registered settlement wallet from the merchant's claim list. After
deregistration, settlements that would have routed to this address no longer
do — set a new primary via
[`POST /v2/merchant/wallet/claim`](/api-reference/public/monetization/wallet-claim)
before settling fresh revenue.
Live on dev as of 2026-06-14; PROD ETA after next GTFU. Shipped via PR
[#2103](https://github.com/droplinked/droplinked-backend/pull/2103) (Phase 5.4.7).
## DELETE /v2/merchant/wallet/:walletAddress
### Authentication
| Guard | Requirement |
| ----- | -------------------------------------------------------------- |
| JWT | Required, any authenticated merchant role |
| Scope | `merchantId` derived from JWT `sub` — **not** from URL / query |
Obtain a merchant JWT via `POST /merchant/admin/login` — see
[Authentication](/authentication).
### Path parameters
| Param | Type | Required | Description |
| --------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `walletAddress` | string | Yes | EVM address to deregister, `0x` + 40 hex chars. Case-insensitive — `0xB272…` and `0xb272…` match the same row via EIP-55 dedup. |
### Query parameters
| Param | Type | Required | Default | Description |
| --------- | ------ | -------- | ------- | --------------------------------------------- |
| `network` | string | No | `base` | Settlement network. MVP supports `base` only. |
### Example
```bash theme={null}
curl -X DELETE \
"https://apiv3.droplinked.com/v2/merchant/wallet/0xB2721aD4F1c4dD8fE45F3F3c8e4F8c8c5d5f1eA9?network=base" \
-H "Authorization: Bearer "
```
### Response — 200 OK (wallet existed)
```json theme={null}
{
"deregistered": true,
"walletAddress": "0xB2721aD4F1c4dD8fE45F3F3c8e4F8c8c5d5f1eA9",
"network": "base"
}
```
### Response — 200 OK (wallet never claimed)
```json theme={null}
{
"deregistered": false,
"walletAddress": "0xB2721aD4F1c4dD8fE45F3F3c8e4F8c8c5d5f1eA9",
"network": "base"
}
```
### Fields
| Field | Type | Nullable | Description |
| --------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `deregistered` | boolean | No | `true` when a row was actually removed. `false` when the address was not registered for this `(merchantId, network)` — the call still returns `200` (see Notes). |
| `walletAddress` | string | No | Echoed input address. |
| `network` | string | No | Echoed network. |
### Errors
| Status | Body | When |
| ------ | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `400` | `{ "statusCode": 400, "status": "failed", "data": { "message": "walletAddress must match the EVM 0x+40-hex shape" } }` | `walletAddress` fails the `^0x[a-fA-F0-9]{40}$` shape check, or `network` is anything other than `base`. |
| `401` | `{ "statusCode": 401, "status": "failed", "data": { "message": "Unauthorized" } }` | Missing or invalid JWT. |
| `5xx` | `{ "statusCode": 500, "status": "failed", ... }` | DB delete failure. |
### Notes
* **Idempotent on the dedup key.** Deregistering a wallet that was never
claimed (or has already been removed) returns `200` with
`deregistered: false`. Clients can safely retry without checking
membership first. This mirrors the `POST` idempotency contract — the
pair forms a clean upsert / delete API.
* **Mixed-case lookup.** Path-parameter casing is normalized to lowercase
before lookup, so `0xB272…eA9` and `0xb272…ea9` resolve to the same row.
* **Primary-flag side-effect.** Deregistering the current primary leaves
the merchant with no primary on that network. Settlement routing falls
back to the merchant's account-level settlement target (if configured)
or the platform escrow until a new primary is claimed.
* **No `404` for unknown rows.** Choice is deliberate: the dedup key is
the natural identity of the resource, and "not present" is a valid
terminal state of `DELETE` for that identity. Returning `404` would
force every client to add a pre-check; instead the response itself
carries the membership signal via `deregistered`.
* **Auth scope.** A merchant cannot deregister another merchant's wallet
even if they know the address — the `merchantId:network:wallet`
composite is the lookup key and `merchantId` always comes from the JWT.
* **Audit trail.** Every deregister emits a `PlatformAuditEvent`
(`OPERATOR_ACTION`, subjectType `MERCHANT_WALLET_CLAIM`) for the
compliance trail, including the no-op cases where `deregistered: false`.
## Related
* [Register Settlement Wallet](/api-reference/public/monetization/wallet-claim) — `POST` counterpart that creates / re-verifies a wallet entry.
* [List Registered Wallets](/api-reference/public/monetization/wallet-list) — `GET` counterpart for the current set of registered wallets.
* [x402 Earnings (Merchant)](/api-reference/public/monetization/x402-earnings) — per-merchant rollup of settlement events affected by deregistration.
# List Registered Wallets (Merchant)
Source: https://docs.droplinked.com/api-reference/public/monetization/wallet-list
List every settlement wallet the authenticated merchant has registered across all networks. Cache-Control: private, max-age=30.
`GET /v2/merchant/wallet/registered` returns the list of settlement wallets
the authenticated merchant has registered via
[`POST /v2/merchant/wallet/claim`](/api-reference/public/monetization/wallet-claim).
This endpoint backs the merchant **Wallets** panel in the dashboard.
Live on dev as of 2026-06-14; PROD ETA after next GTFU. Shipped via PR
[#2103](https://github.com/droplinked/droplinked-backend/pull/2103) (Phase 5.4.7).
## GET /v2/merchant/wallet/registered
### Authentication
| Guard | Requirement |
| ----- | --------------------------------------------------------------- |
| JWT | Required, any authenticated merchant role |
| Scope | `merchantId` derived from JWT `sub` — **not** from query / body |
Obtain a merchant JWT via `POST /merchant/admin/login` — see
[Authentication](/authentication). Any `?merchantId=` query parameter is
silently ignored.
### Query parameters
None. The response is always the full list for the JWT-bound merchant
across all networks.
### Example
```bash theme={null}
curl "https://apiv3.droplinked.com/v2/merchant/wallet/registered" \
-H "Authorization: Bearer "
```
### Response — 200 OK
```json theme={null}
{
"wallets": [
{
"walletAddress": "0xB2721aD4F1c4dD8fE45F3F3c8e4F8c8c5d5f1eA9",
"network": "base",
"label": "Mainnet Treasury",
"isPrimary": true,
"claimedAt": "2026-06-14T17:30:00.000Z",
"lastVerifiedAt": "2026-06-14T17:31:00.000Z"
}
],
"asOf": "2026-06-14T17:31:05.220Z"
}
```
### Fields
| Field | Type | Nullable | Description |
| -------------------------- | --------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `wallets` | array | No | One entry per registered wallet for the merchant, across all networks. Empty array for merchants who have never claimed a wallet. |
| `wallets[].walletAddress` | string | No | The address as the merchant originally submitted it (EIP-55 case preserved). |
| `wallets[].network` | string | No | Settlement network (`base` in MVP). |
| `wallets[].label` | string | Yes | Operator-supplied label. `null` when none was supplied. |
| `wallets[].isPrimary` | boolean | No | Whether this row is the primary settlement target on `network`. At most one per `(merchantId, network)`. |
| `wallets[].claimedAt` | ISO-8601 string | No | Original claim timestamp (UTC). |
| `wallets[].lastVerifiedAt` | ISO-8601 string | No | Most-recent claim-call timestamp (UTC). Refreshed on every re-claim. |
| `asOf` | ISO-8601 string | No | Snapshot timestamp (server clock, UTC). |
### Errors
| Status | Body | When |
| ------ | ---------------------------------------------------------------------------------- | -------------------------------------- |
| `401` | `{ "statusCode": 401, "status": "failed", "data": { "message": "Unauthorized" } }` | Missing or invalid JWT |
| `5xx` | `{ "statusCode": 500, "status": "failed", ... }` | Hard backend failure — see Notes below |
### Notes
* **Caching.** The response carries `Cache-Control: private, max-age=30`.
Clients should expect up to a 30s lag after a claim / deregister before
the change is visible if the response was cached by an intermediate.
* **Auth scope.** The endpoint reads `merchantId` from the JWT `sub` claim
only. Any `?merchantId=` query parameter is silently ignored. A merchant
cannot read another merchant's wallets through this endpoint.
* **Ordering.** Rows are sorted by `claimedAt` descending — most-recently
claimed first. Primary status does not affect order; check `isPrimary`
to identify the routed-to wallet.
* **Fail-open semantics.** If the underlying read throws, the endpoint
returns the empty-state envelope (`wallets: []`) with `200 OK` rather
than propagating the error. Inspect server logs / Sentry for the
underlying failure. Merchants never see a partial Wallets-panel error
in the dashboard.
* **Across networks.** The list is **not** filtered by network. A merchant
registered on `base` today and a future second network tomorrow will
see both rows here; clients filter client-side as needed.
## Related
* [Register Settlement Wallet](/api-reference/public/monetization/wallet-claim) — `POST` counterpart that creates / re-verifies an entry in this list.
* [Deregister Wallet](/api-reference/public/monetization/wallet-deregister) — removes a row from this list.
* [x402 Earnings (Merchant)](/api-reference/public/monetization/x402-earnings) — per-merchant rollup of settlement events that route to the primary wallet.
# x402 Earnings (Merchant)
Source: https://docs.droplinked.com/api-reference/public/monetization/x402-earnings
Per-merchant rollup of x402 settlement events. Reads X402SettlementLog scoped to the authenticated merchant. Returns empty rows until X402_ENABLED is flipped on the platform. Fail-open semantics.
`GET /v2/merchant/x402-earnings` returns the authenticated merchant's rollup of
x402 settlement events: per-row settlement detail, totals envelope (settlement
amount + count), and pagination. This endpoint is the data source for the
merchant **x402 Earnings** page in the dashboard.
This endpoint requires:
* **Merchant JWT** — any authenticated merchant role (`OWNER`, `MEMBER`, `PRODUCER`)
The merchant scope is **derived from the JWT `sub` claim**. There is no
`merchantId` query parameter. If a client passes one anyway it is silently
ignored — the response is always scoped to the JWT-bound merchant.
## GET /v2/merchant/x402-earnings
### Authentication
| Guard | Requirement |
| ----- | --------------------------------------------------------------- |
| JWT | Required, any authenticated merchant role |
| Scope | `merchantId` derived from JWT `sub` — **not** from query / body |
Obtain a merchant JWT via `POST /merchant/admin/login` — see
[Authentication](/authentication).
### Query parameters
| Param | Type | Required | Default | Description |
| ----------- | --------------- | -------- | ------- | ----------------------------------------------------------------------------- |
| `page` | integer | No | `1` | 1-indexed page number. |
| `limit` | integer | No | `20` | Rows per page. |
| `startDate` | ISO-8601 string | No | — | Filter to settlements with `settledAt >= startDate`. Omit for no lower bound. |
| `endDate` | ISO-8601 string | No | — | Filter to settlements with `settledAt <= endDate`. Omit for no upper bound. |
### Example
```bash theme={null}
curl "https://apiv3.droplinked.com/v2/merchant/x402-earnings?page=1&limit=20" \
-H "Authorization: Bearer "
```
With a date filter:
```bash theme={null}
curl "https://apiv3.droplinked.com/v2/merchant/x402-earnings?page=1&limit=20&startDate=2026-01-01T00:00:00Z&endDate=2026-06-14T23:59:59Z" \
-H "Authorization: Bearer "
```
### Response — 200 OK
```json theme={null}
{
"rows": [],
"totalSettlementsUsdCents": 0,
"totalCount": 0,
"asOf": "2026-06-14T23:41:10.180Z",
"pagination": {
"page": 1,
"limit": 20,
"total": 0
}
}
```
### Fields
| Field | Type | Nullable | Description |
| -------------------------- | --------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rows` | array | No | One entry per x402 settlement event matching the filter. Empty array until `X402_ENABLED` is flipped on, or when the merchant has no settlements in the filter window. |
| `rows[].settlementId` | string | No | `X402SettlementLog` row `_id`. |
| `rows[].settledAt` | ISO-8601 string | No | Settlement timestamp (UTC) — when the x402 settlement event was captured on chain and recorded. |
| `rows[].amountUsdCents` | integer | No | Settlement amount in **USD cents**, integer. x402 settles natively in USDC on Base; the read path snapshots the FX at settlement time. |
| `rows[].productId` | string | Yes | Source product `_id` if the settlement is tied to a specific product. `null` for storefront-wide payouts. |
| `rows[].orderId` | string | Yes | Source order `_id` if available. `null` for legacy rows. |
| `rows[].txHash` | string | Yes | Base transaction hash. `null` for legacy rows pre-dating chain-hash capture. |
| `totalSettlementsUsdCents` | integer | No | Sum of `amountUsdCents` for **the merchant across the filter window** (not just the current page). |
| `totalCount` | integer | No | Total settlement count for the merchant across the filter window. |
| `asOf` | ISO-8601 string | No | Snapshot timestamp (server clock, UTC). |
| `pagination` | object | No | Pagination envelope. |
| `pagination.page` | integer | No | Echoed `page` query param (1-indexed). |
| `pagination.limit` | integer | No | Echoed `limit` query param. |
| `pagination.total` | integer | No | Total number of settlement rows matching the filter (across pages). Equal to `totalCount`. |
### Errors
| Status | Body | When |
| ------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `401` | `{ "statusCode": 401, "status": "failed", "data": { "message": "Unauthorized" } }` | Missing or invalid JWT |
| `5xx` | `{ "statusCode": 500, "status": "failed", ... }` | Hard backend failure — see Notes below for the fail-open contract |
### Notes
* **`X402_ENABLED` gating.** Until the platform flag `X402_ENABLED` is set to
`true`, `X402SettlementLog` accepts no writes and this endpoint always
returns `rows: []`, `totalSettlementsUsdCents: 0`, `totalCount: 0`. The
endpoint itself is always reachable — the gate is at the write path, not
the read path. Merchants safely linkable to the page before the flip; it
renders an empty-state.
* **Auth scope.** The endpoint reads `merchantId` from the JWT `sub` claim
only. Any `?merchantId=` is silently ignored. A merchant cannot read
another merchant's settlements through this endpoint — even with a
syntactically valid query parameter pointing at the target.
* **Fail-open semantics.** If the underlying `X402SettlementLog` aggregation
throws, the endpoint returns the empty-state envelope (`rows: []`,
totals `0`) with `200 OK` rather than propagating the error. Inspect server
logs / Sentry for the underlying failure.
* **Currency.** All monetary fields are **USD cents, integer** — same contract
as the [admin x402-earnings rollup](/api-reference/admin/monetization/x402-earnings).
Divide by `100` for major-unit display.
* **Pagination.** `total` and `totalCount` count **settlement rows**, not
products / orders. A merchant with 50 settlements across 3 products
reports `total: 50`, `totalCount: 50`.
* **Date filter semantics.** `startDate` and `endDate` are inclusive bounds on
`settledAt`. Omit either to leave that side of the window unbounded; omit
both for the full per-merchant history (capped at the merchant's earliest
settlement).
## Related
* [Billing Invoices (Merchant)](/api-reference/public/monetization/billing-invoices) — companion merchant-facing billing history (trailing 365 days, paid subscription invoices).
* [x402 Earnings (Admin)](/api-reference/admin/monetization/x402-earnings) — admin-side per-merchant x402 settlement rollup (network-wide view).
* [Platform Fee Summary (Admin)](/api-reference/admin/monetization/platform-fee-summary) — network-wide MRR / ARR / 30d / 365d rollup that includes x402 in `revenue30dUsdCents` / `revenue365dUsdCents`.
# Validate a redeem code (discount or gift card)
Source: https://docs.droplinked.com/api-reference/public/redeem-validation
Pre-validate a discount code or gift-card code before applying it at checkout. Fail-open posture — backend faults return HTTP 200 with a structured error envelope so the FE renders inline retry instead of crashing the redeem field.
Validate a discount code or gift-card code before applying it at checkout. Both endpoints share a single request envelope (`code` + `shopId` + `cartTotalCents` + `currency`) so a single unified redeem field on the storefront can swap the URL path based on which kind the buyer typed.
**Fail-open posture.** Backend faults return **HTTP 200** with `{ valid: false, error: "service_unavailable" }` — never a 5xx. The redeem field is payment-adjacent and 5xx-ing it would leave the buyer with a broken checkout. The FE renders an inline error + retry CTA instead of crashing.
These endpoints are **read-only**. They never bump a discount's `timesUsed` counter or flip a gift card's `isRedeemed` state. Actual redemption happens atomically with payment authorization at the [checkout-intent resolver](/api-reference/public/checkout-intent-resolver) chokepoint when the FE submits the cart with the validated code field set.
## When to use this
* A storefront or partner checkout renders a **unified redeem field** that accepts either a discount code or a gift-card code, and dispatches to the matching validator based on which path the buyer picks (or based on inline heuristics in the FE)
* A custom checkout wants to preview the redeem amount in the cart summary **before** the buyer hits "Pay" — so the buyer sees the same total at validation, at the cart summary, and at the PSP authorization step
* The FE needs a stable typed error envelope (rather than HTTP status branching) so it can surface inline messages without try/catching every fetch
Call this once when the buyer enters a code, before enabling the "Apply" CTA. The endpoint never mutates state — calling it repeatedly is safe.
## Authentication
None — both endpoints are public. Decorated with `@Public()` on the controller and mounted under `/v2/redeem/*`.
## Shared request body
Both endpoints accept the same `RedeemValidationDto` payload.
| Field | Type | Required | Description |
| ---------------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `code` | string (1–64 chars) | Yes | The redemption code as the buyer typed it. Case-insensitive on the server side. |
| `shopId` | string | Yes | The shop the code belongs to (`Shop._id`). |
| `cartTotalCents` | int (>= 0) | Yes | Pre-redeem cart subtotal in the shop's currency, in **cents** (minor units). |
| `currency` | string (1–8 chars) | Yes | ISO-4217 currency code (e.g. `USD`). Informational for the discount path (the discount row carries its own currency for FIXED\_AMOUNT codes) and ignored for the gift-card path (gift-card balance is currency-less per the v1 schema). Required on the wire so the FE contract matches the unified field exactly. |
| `appliedCodes` | string\[] | No | Codes already applied to the cart. When supplied, a same-code reapply short-circuits to `already_applied` without a service round-trip. v1 of the shop-builder unified redeem field does not send this — safe to omit. |
## Shared response envelope
Both endpoints return the same union response shape at **HTTP 200** for every business outcome.
**Success — `valid: true`:**
```json theme={null}
{
"valid": true,
"kind": "discount",
"code": "FREESHIP10",
"valueCents": 1000,
"label": "$10 off shipping"
}
```
| Field | Type | Notes |
| ------------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `valid` | `true` | Code is eligible against this cart shape. |
| `kind` | `"discount"` \| `"giftcard"` | Mirrors which endpoint was hit — useful for typed reducers on the FE. |
| `code` | string | Normalized echo of the redemption code (canonical case as stored). |
| `valueCents` | int | Discount amount applied to this cart, in cents. **Capped** server-side so it never exceeds `cartTotalCents` (you'll never refund more than the cart is worth). |
| `label` | string | Human-readable name surfaced as a confirmation pill on the order summary (e.g. `"$10 off shipping"`, `"Gift card balance applied"`). |
**Error — `valid: false`:**
```json theme={null}
{
"valid": false,
"error": "min_cart_not_met",
"minCartCents": 5000
}
```
| Field | Type | Notes |
| -------------- | ------- | --------------------------------------------------------------------------------------------------------------- |
| `valid` | `false` | Code is structurally well-formed but not eligible against this cart. |
| `error` | enum | One of the values in the error-code table below. |
| `minCartCents` | int | **Only set on `error: "min_cart_not_met"`** — the threshold the FE should render as "spend \$X more to unlock". |
## Error codes
The `error` enum is **stable** — new codes will only be added, never renamed. FE clients should default to a generic "This code can't be applied" string when an unknown reason arrives.
| `error` | Returned by | When | Suggested FE message |
| --------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `invalid_code` | discount + giftcard | Code unrecognized, disabled, expired, not-yet-active, exhausted, or out-of-scope for the cart's products. The endpoint **deliberately collapses** every promo-engine lifecycle failure into a single public reason (see [Architecture notes](#architecture-notes)). | "That code isn't recognized. Check the spelling and try again." |
| `already_applied` | discount + giftcard | The same `code` already appears in the buyer's `appliedCodes` array. | "You've already applied this code." |
| `min_cart_not_met` | discount only | Discount has a minimum-purchase rule and `cartTotalCents < minPurchaseCents`. The response carries `minCartCents` so the FE can render "spend \$X more". | "Add more to your cart to use this code." |
| `zero_balance` | giftcard only | Gift card exists but its remaining balance is `0` (fully spent). | "This gift card has no balance left." |
| `expired` | giftcard only | Gift card exists but its expiry timestamp has passed. | "This gift card has expired." |
| `service_unavailable` | discount + giftcard | Backend fault while calling into the discount engine or gift-card repo. **Fail-open envelope** — the FE should render an inline "Try again" CTA. | "Something went wrong validating that code. Try again." |
## `POST /v2/redeem/discount/validate`
Validates a merchant-issued coupon code against the cart's total.
### Curl example
```bash theme={null}
curl -X POST https://apiv3.droplinked.com/v2/redeem/discount/validate \
-H 'content-type: application/json' \
-d '{
"code": "FREESHIP10",
"shopId": "65f8000000000000000000aa",
"cartTotalCents": 9999,
"currency": "USD"
}'
```
### Success response
```json theme={null}
{
"valid": true,
"kind": "discount",
"code": "FREESHIP10",
"valueCents": 1000,
"label": "$10 off shipping"
}
```
### Min-purchase failure response
```json theme={null}
{
"valid": false,
"error": "min_cart_not_met",
"minCartCents": 5000
}
```
### Invalid-code failure response
```json theme={null}
{
"valid": false,
"error": "invalid_code"
}
```
## `POST /v2/redeem/giftcard/validate`
Validates a gift-card code against the cart's total. Same request envelope; `kind: "giftcard"` on success.
### Curl example
```bash theme={null}
curl -X POST https://apiv3.droplinked.com/v2/redeem/giftcard/validate \
-H 'content-type: application/json' \
-d '{
"code": "GIFTABC1234",
"shopId": "65f8000000000000000000aa",
"cartTotalCents": 9999,
"currency": "USD"
}'
```
### Success response
```json theme={null}
{
"valid": true,
"kind": "giftcard",
"code": "GIFTABC1234",
"valueCents": 2500,
"label": "Gift card balance applied"
}
```
### Zero-balance failure response
```json theme={null}
{
"valid": false,
"error": "zero_balance"
}
```
### Expired failure response
```json theme={null}
{
"valid": false,
"error": "expired"
}
```
## Schema-validation errors (`400`)
The only non-200 status either endpoint emits is `400` for **structurally malformed** requests (missing required field, negative `cartTotalCents`, `code` longer than 64 chars, etc.). A malformed request is a structural FE bug that a retry can't fix — so the fail-open envelope does not apply.
| Status | When |
| ------ | ----------------------------------------------------------------------------------------------------------------------- |
| `200` | Every business outcome — success **or** typed error (`valid: false`). Branch on the response body, not the status code. |
| `400` | Schema validation failed on the request body. |
## Architecture notes
### Reason-code collapsing (discount internals)
The underlying discount engine surfaces a rich internal reason union — `code_not_found` / `code_disabled` / `code_expired` / `code_not_yet_active` / `code_exhausted` / `product_not_in_scope`. The public `/v2/redeem/discount/validate` surface **deliberately collapses all six into `invalid_code`**.
This follows the Apple / Shopify convention of not leaking promotion-engine internals to the buyer:
* Telling an enumeration attacker which exact code is `disabled` vs `expired` widens the leak surface
* The FE can't render anything different for "disabled" vs "expired" anyway — the UX outcome is identical
* The internal reason is preserved in server logs for the merchant dashboard
`min_purchase_not_met` is the one discount internal reason that **does** surface (as `min_cart_not_met` + `minCartCents`) — because the FE can render a useful "spend \$X more to unlock" prompt the buyer can act on.
### Gift-card disambiguation
The gift-card repo's `getGiftCardByCode` returns `null` across multiple failure modes (missing / fully-redeemed / expired). To preserve a useful FE surface, the service runs a best-effort secondary lookup on the null path to disambiguate `zero_balance` vs `expired` vs `invalid_code`. The disambiguation walk is wrapped in its own try/catch so transient gift-card-repo faults cannot escape the fail-open envelope — the worst case is the public reason collapses to `invalid_code`.
### Fail-open: backend errors return HTTP 200
Both validators wrap the engine call in try/catch and return `{ valid: false, error: "service_unavailable" }` at **HTTP 200** on any backend fault. The redeem field is payment-adjacent — 5xx-ing it would leave buyers with a broken checkout. The FE renders an inline retry CTA and the buyer keeps going. This matches the PSP-stability backbone discipline (the same posture the resolver/gate path uses).
The only exception is request-schema validation — a malformed body returns `400`, because a structural FE bug is not something a retry can fix.
### Redemption happens at the checkout-intent chokepoint
These endpoints are a **precondition** for redemption, not the write. The actual mutation (decrementing remaining uses on a discount, marking a gift card as `isRedeemed`, applying the line-item discount to the order) happens server-side at the [checkout-intent resolver](/api-reference/public/checkout-intent-resolver) chokepoint when the FE submits the cart with the validated code field set.
That single chokepoint design means:
* No double-redemption is possible — the redemption write is atomic with payment authorization
* These validators can be called repeatedly without side effects
* The FE never has to "release" a code on cart-abandonment — nothing was ever locked
### Known v1 limitation: line items not in the unified envelope
The unified `RedeemValidationDto` does **not** carry line items. Discounts scoped to specific products (`PRODUCT_IDS` scope) will therefore validate as `invalid_code` on this surface — the public collapse layer doesn't expose `product_not_in_scope` separately. This matches the v1 shape of the shop-builder unified redeem field, which doesn't submit line items.
If you need product-scoped discount validation today, call the original [`POST /v2/discounts/validate`](/api-reference/public/discounts-validate) endpoint — it carries `lineItems` and returns the granular reason union directly.
## Related
The chokepoint where validated discount + gift-card codes are atomically redeemed against payment authorization. These validators are a precondition — the resolver is the write.
The original discount-only validator. Carries `lineItems` and returns the full reason union (`code_expired` / `code_not_yet_active` / `product_not_in_scope` / etc.) without the public collapse layer. Use when you need product-scope validation.
The storefront-side endpoint customers hit from the recovery email link. Often paired with redeem-field pre-validation when a recovery email carries a bounce-back coupon.
Where the resolver's atomic redemption write fits into the full order timeline (cart → intent → PSP authorization → order created).
# Service-Provider Routing Recommendation
Source: https://docs.droplinked.com/api-reference/public/service-provider-routing
WMS/3PL partner recommendation — returns the ordered list of ACTIVE service providers by archetype, sorted by track-record (successfulIngestionCount desc, ties broken by most-recent success).
`GET /v2/service-provider-routing/recommend` is the InventoryOS counterpart to lender routing — it returns the ordered list of ACTIVE WMS/3PL providers a merchant should route an inventory ingestion / fulfillment task to.
This endpoint is **public** and **read-only**. Routes nothing — the actual ingestion path stays gated on `ServiceProviderRegistry.isActive` + the HMAC webhook guard.
## Query parameters
| Param | Type | Default | Notes |
| ----------- | ---- | ------- | ----------------------------------------------- |
| `archetype` | enum | (all) | `stord` \| `flexport` \| `shipbob` \| `generic` |
| `limit` | int | `10` | Clamped to `[1, 50]` |
## Ranking
1. **`successfulIngestionCount` desc** — proven track record first
2. **Ties broken by most-recent successful ingestion** (`lastSuccessfulIngestionAt`)
## Example
```bash theme={null}
curl 'https://apiv3.droplinked.com/v2/service-provider-routing/recommend?archetype=stord&limit=5'
```
```json theme={null}
{
"archetype": "stord",
"count": 1,
"recommendations": [
{
"providerId": "stord-us-east-1",
"displayName": "Stor'd US-East",
"archetype": "stord",
"successfulIngestionCount": 47,
"lastSuccessfulIngestionAt": "2026-06-11T14:30:00Z",
"rank": 1
}
]
}
```
## Related
* [Lender Routing Recommendation](/api-reference/public/lender-routing) — same pattern for lenders
* MCP tool: `recommend_service_provider` (wraps this endpoint)
# Shopify webhook ingest
Source: https://docs.droplinked.com/api-reference/public/shopify-webhook-ingest
Receive product + order webhooks from a connected Shopify store. HMAC-validated; rejects unauthed traffic with 401.
`POST /v2/integrations/shopify/webhook/{topic}` is the **public** wire-protocol endpoint
Shopify webhooks land on after a merchant connects their store to droplinked. It is the
hot path that keeps droplinked's InventoryOS catalog mirror in sync with the merchant's
Shopify store of record.
This page documents the **wire protocol** for engineers configuring Shopify webhooks
themselves. For the merchant-facing integration walkthrough — how to connect a store,
authorize the droplinked Shopify app, and verify the sync is healthy — see
[Connect your Shopify store](/agentic/connect-shopify).
Although this endpoint has no JWT and no IP allowlist, it is **not** an open ingest: every
request is HMAC-validated against the per-shop webhook secret that droplinked issued at
connection time. Unauthed traffic is rejected with a 401.
## Supported topics
The `{topic}` path segment selects the handler. Currently wired:
| Topic | Behavior |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `products-create` | Mirror the new product into droplinked's InventoryOS catalog. Idempotent on `shopify.product.id` |
| `products-update` | Upsert the product mirror with the latest title, variants, images, and inventory |
| `products-delete` | Soft-remove the product from the mirror (preserves history; hides from the catalog feed) |
| `orders-create` | Log the order event. v1 is a stub — v2 will fire affiliate-attribution and repayment-attestation side effects |
| `orders-updated` | Log the order update event. v1 stub — v2 will reconcile attribution state when an order moves through fulfillment / refund states |
Additional topics will be added as the integration matures. Unknown topics return 404 with
`message: "shopify_webhook_topic_not_supported"`.
## HMAC validation
Shopify computes an SHA-256 HMAC over the **raw request body** using the per-shop webhook
secret and sends the result in the `X-Shopify-Hmac-Sha256` header. Droplinked re-computes
the expected HMAC server-side using the secret it stored at connection time and
constant-time-compares against the header.
| Header | Description |
| ----------------------- | -------------------------------------------------------------------------------------------- |
| `X-Shopify-Hmac-Sha256` | Base64-encoded SHA-256 HMAC of the raw body, keyed on the per-shop webhook secret |
| `X-Shopify-Shop-Domain` | The source shop's `.myshopify.com` domain — used to look up which secret to validate against |
| `X-Shopify-Topic` | Informational — droplinked routes on the `{topic}` path segment, not this header |
| `Content-Type` | `application/json` |
On HMAC mismatch the endpoint returns **401** with reason `shopify_webhook_auth_failed`.
The body is not parsed when validation fails, so no side effects fire.
If the `X-Shopify-Shop-Domain` does not resolve to a connected shop, the endpoint returns
**404** with a message instructing the operator to call `POST /admin/shopify-integration/connect`
first.
## Configuring the webhook in Shopify Admin
In Shopify Admin → **Settings** → **Notifications** → **Webhooks**, add one webhook per
supported topic. For each:
| Field | Value |
| ---------------------- | -------------------------------------------------------------------------- |
| Event | The matching Shopify event (e.g. *Product creation* for `products-create`) |
| Format | **JSON** |
| URL | `https://apiv3.droplinked.com/v2/integrations/shopify/webhook/` |
| Webhook API version | Latest stable (currently `2026-04`) |
| Webhook signing secret | Provided by droplinked on `POST /admin/shopify-integration/connect` |
The URL `` segment **must match exactly** one of the supported topics in the table
above. Example URLs:
```
https://apiv3.droplinked.com/v2/integrations/shopify/webhook/products-create
https://apiv3.droplinked.com/v2/integrations/shopify/webhook/products-update
https://apiv3.droplinked.com/v2/integrations/shopify/webhook/products-delete
https://apiv3.droplinked.com/v2/integrations/shopify/webhook/orders-create
https://apiv3.droplinked.com/v2/integrations/shopify/webhook/orders-updated
```
## Example request
A Shopify-emitted `products-create` webhook lands on droplinked as:
```http theme={null}
POST /v2/integrations/shopify/webhook/products-create HTTP/1.1
Host: apiv3.droplinked.com
Content-Type: application/json
X-Shopify-Topic: products/create
X-Shopify-Shop-Domain: example-merchant.myshopify.com
X-Shopify-Hmac-Sha256: aB3...=
X-Shopify-Webhook-Id: 1234567890
X-Shopify-Triggered-At: 2026-06-13T18:00:00.000Z
{
"id": 987654321,
"title": "Limited Edition Tee",
"vendor": "Example Merchant",
"product_type": "Apparel",
"variants": [
{
"id": 11111,
"sku": "TEE-RED-M",
"price": "29.99",
"inventory_quantity": 42
}
],
"images": [{ "src": "https://cdn.shopify.com/.../tee.jpg" }]
}
```
### Response — 200 OK
```json theme={null}
{ "ok": true }
```
### Response — 401 Unauthorized (HMAC mismatch)
```json theme={null}
{
"statusCode": 401,
"message": "shopify_webhook_auth_failed"
}
```
### Response — 404 Not Found (shop not connected)
```json theme={null}
{
"statusCode": 404,
"message": "Shopify shop domain not registered with droplinked. Call POST /admin/shopify-integration/connect first."
}
```
### Response — 404 Not Found (unsupported topic)
```json theme={null}
{
"statusCode": 404,
"message": "shopify_webhook_topic_not_supported"
}
```
### Response — 400 Bad Request (malformed body)
```json theme={null}
{
"statusCode": 400,
"message": "shopify_webhook_body_invalid"
}
```
Returned when HMAC validates but the JSON cannot be parsed or is missing required fields
for the chosen topic.
## Body forwarding and normalization
After HMAC validation, droplinked normalizes the Shopify product shape into its
[InventoryOS](/concepts/inventory-os) catalog model. The mapping is:
| Shopify field | InventoryOS field |
| ------------------------------- | -------------------------------------------- |
| `id` | `externalSourceIds.shopify.productId` |
| `title` | `title` |
| `vendor` | `brand` |
| `variants[].id` | `skus[].externalSourceIds.shopify.variantId` |
| `variants[].sku` | `skus[].sku` |
| `variants[].price` | `skus[].priceCents` (×100) |
| `variants[].inventory_quantity` | `skus[].stockOnHand` |
| `images[].src` | `images[].url` |
The Shopify store remains the **source of truth** — droplinked's mirror is a
read-projection. Edits made in droplinked's admin UI for Shopify-sourced products are
rejected; merchants edit in Shopify and the webhook reconciles the mirror.
## Operator setup
Before pointing Shopify webhooks at this endpoint, the merchant **must** be connected via
the admin API:
`POST /admin/shopify-integration/connect` registers the shop's `*.myshopify.com`
domain, issues a per-shop webhook signing secret, and links the Shopify store to a
droplinked shop record.
The webhook signing secret is returned in the response body of the connect call. It is
only retrievable once — re-running connect rotates the secret and invalidates the old
one.
Following the table above, the merchant adds one webhook per supported topic using the
issued secret as the signing secret.
Create a draft product in Shopify. Droplinked should receive the `products-create`
webhook within seconds and mirror the product into InventoryOS. The connection-health
surface in admin shows the most recent successful webhook timestamp per topic.
If the merchant skips the connect step and Shopify starts sending webhooks anyway, every
webhook will 404 with the "shop domain not registered" message above. Shopify will retry
on its own backoff schedule.
## Related
* [Connect your Shopify store](/agentic/connect-shopify) — merchant-facing integration
walkthrough.
* [InventoryOS](/concepts/inventory-os) — the catalog model the webhook body is normalized
into.
# Mint storefront preview token
Source: https://docs.droplinked.com/api-reference/public/storefront-preview-token
Short-lived signed token for previewing your storefront draft in the Template Designer iframe. Merchant-scoped, 10-minute TTL, HMAC-SHA-256 verified.
`POST /v2/storefront/preview-token` mints a short-lived signed token that the **Template Designer Live Preview iframe** uses to render the merchant's draft storefront. The token is bound to a specific shop and expires after 10 minutes.
Closes Phase 3 of DEV-AUDIT #199. The Template Designer Live Preview surface in the shop-builder dashboard calls this endpoint, embeds the returned `previewUrl` into an iframe, and the storefront verifier (separate ticket) HMAC-checks the token before rendering the draft.
## When to use this
* The Template Designer's "Live Preview" CTA — mint a token, embed `previewUrl` into the iframe `src`
* Any merchant-facing tool that needs a time-bounded preview link to a draft storefront
* **Do NOT** use this for production publishing — published storefronts don't need a token
## Authentication
Merchant JWT (`MerchantOnlyJwtGuard`). The endpoint also asserts `req.user.shopId === body.shopId` at the controller layer — a merchant can only mint preview tokens for **their own** shop. Cross-shop mint attempts return `403`.
## Request
```
POST /v2/storefront/preview-token
```
Body:
| Field | Type | Notes |
| -------- | ------ | ----------------------------------------------------- |
| `shopId` | string | The shop's Mongo `_id`. MUST equal `req.user.shopId`. |
### Curl example
```bash theme={null}
curl -s -X POST "https://apiv3.droplinked.com/v2/storefront/preview-token" \
-H "Authorization: Bearer $MERCHANT_JWT" \
-H "Content-Type: application/json" \
-d '{"shopId":"66c0fe34a9f1b1d3e1c2b001"}' | jq .
```
## Response (200)
```json theme={null}
{
"previewToken": "eyJzaG9wSWQiOiI2NmMwZmUzNGE5ZjFiMWQzZTFjMmIwMDEiLCJleHBpcmVzQXQiOiIyMDI2LTA2LTE0VDEyOjM0OjU2LjAwMFoiLCJraW5kIjoic3RvcmVmcm9udC1wcmV2aWV3In0.a1b2c3d4e5f6...",
"expiresAt": "2026-06-14T12:34:56.000Z",
"previewUrl": "https://droplinked.io/demo-shop/_preview?token=eyJzaG9wSWQiOiI2NmMw..."
}
```
### Field reference
| Field | Type | Notes |
| -------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `previewToken` | string | Signed token in format `.` (see Token format below). |
| `expiresAt` | ISO-8601 string | 10 minutes from mint. |
| `previewUrl` | string | Full URL with token embedded — drop straight into iframe `src`. Uses `droplinked.io` (NOT `.com`) per storefront URL discipline. |
## Token format
`.` where:
* `payload` — UTF-8 JSON `{ shopId: string, expiresAt: ISO-8601 string, kind: "storefront-preview" }` then base64url-encoded
* `sig` — HMAC-SHA-256 of the payload bytes under `PREVIEW_TOKEN_SECRET` (or `JWT_SECRET` fallback for dev), then base64url-encoded
The two parts are joined with a `.` separator. Shopfront verifier:
```ts theme={null}
import { createHmac } from 'crypto'
function verifyPreviewToken(token: string, secret: string): { shopId: string; expiresAt: Date } | null {
const [payloadB64, sigB64] = token.split('.')
if (!payloadB64 || !sigB64) return null
const expectedSig = createHmac('sha256', secret)
.update(payloadB64)
.digest('base64url')
if (sigB64 !== expectedSig) return null
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf8'))
if (payload.kind !== 'storefront-preview') return null
const expiresAt = new Date(payload.expiresAt)
if (Number.isNaN(expiresAt.getTime()) || expiresAt < new Date()) return null
return { shopId: payload.shopId, expiresAt }
}
```
## Why HMAC over JWT
The shopfront verifier is a single shared-secret check at the storefront edge. HMAC avoids:
* JWKS round-trips (no key fetch from a JWKS endpoint at request time)
* Asymmetric key rotation surface (no private/public key pair, no kid)
* Edge-cache invalidation on key rotation
* The full jose library dependency at the storefront edge
The fixed-shape payload (`{shopId, expiresAt, kind}`) is also stricter than typical JWT claim taxonomy — `iss`/`aud`/`jti`/`sub` buy nothing for a 10-minute single-purpose token. If we ever need rotation, audience-narrowing, or a kid, the swap is one file behind `PreviewTokenService.mint` — the controller response shape does not change.
## TTL + rate limiting
* **TTL**: 10 minutes from mint. Hard-coded constant `PREVIEW_TOKEN_TTL_MS` in the service.
* **Rate limit**: not applied at v1. The cross-shop guard limits abuse blast radius to "self-DoS your own preview." Revisit if cross-merchant abuse signal appears.
* **Single chokepoint**: all minting goes through `PreviewTokenService.mint` so audit-log + rotation hooks can be added in one place.
## Error responses
| Status | When |
| ------ | ------------------------------------------------------------------------------- |
| `400` | Malformed `shopId` (not a valid Mongo ObjectId). |
| `401` | Missing / invalid JWT. |
| `403` | Cross-shop mint attempt (`req.user.shopId !== body.shopId`). |
| `500` | No `PREVIEW_TOKEN_SECRET` or `JWT_SECRET` configured (server misconfiguration). |
## Related
* [Get abandoned cart details](/api-reference/public/abandoned-cart-details) — sibling merchant-scoped surface
* [Platform fees revenue rollup (admin)](/api-reference/admin/monetization-platform-fees) — sibling admin operator surface
* [Trust fabric stats](/api-reference/public/trust-fabric-stats) — sibling public read surface
# Trust-Fabric Stats
Source: https://docs.droplinked.com/api-reference/public/trust-fabric-stats
Aggregate-only counts of the trust-fabric trinity — registered lenders, service providers, methodology versions, on-chain attestations by schema. Powers partner-facing dashboards without exposing per-row data.
`GET /v2/trust-fabric/stats` returns a platform-wide aggregate roll-up of the [trust-fabric trinity](/concepts/trust-fabric) — the count of registered lenders, registered WMS/3PL service providers, registered methodology versions, and on-chain attestations broken out by schema (A/B/C/D). It's the third-party visualization surface for partner dashboards, agent platform-scale signaling, and freshness probes.
The endpoint is intentionally **aggregate-only**: no per-row data, no PII, no merchant identifiers, no individual lender names. To resolve a specific entity (lender, methodology, service provider), use the per-id endpoints documented elsewhere in this section.
This endpoint exposes only aggregate counts. To resolve a specific lender or methodology, use the per-id endpoints documented elsewhere in this section.
## Request
```
GET /v2/trust-fabric/stats
```
No path params, no query params, no body. No JWT — **public** endpoint.
## Response (200)
```json theme={null}
{
"lenders": {
"total": 2,
"active": 2
},
"serviceProviders": {
"total": 5,
"active": 4
},
"methodologies": {
"totalLenders": 2,
"activeVersions": 1
},
"attestations": {
"schemaA": 12,
"schemaB": 8,
"schemaC": 4,
"schemaD": 2
},
"asOf": "2026-06-12T01:30:00Z"
}
```
## Field reference
### `lenders.*`
| Field | Type | Notes |
| ---------------- | ---- | ------------------------------------------------------------------------------------------------------------------------- |
| `lenders.total` | int | Lifetime count of registered lenders across all lifecycle states (`PENDING_KYB` \| `ACTIVE` \| `SUSPENDED` \| `ARCHIVED`) |
| `lenders.active` | int | Subset of `total` currently in `ACTIVE` state — eligible to mint Schema B credit-risk attestations |
### `serviceProviders.*`
| Field | Type | Notes |
| ------------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------- |
| `serviceProviders.total` | int | Lifetime count of registered WMS/3PL providers across all lifecycle states |
| `serviceProviders.active` | int | Subset of `total` currently `ACTIVE` — eligible to mint Schema D cross-attestations and accept HMAC-gated ingestion webhooks |
### `methodologies.*`
| Field | Type | Notes |
| ------------------------------ | ---- | ---------------------------------------------------------------------------------------------------------------- |
| `methodologies.totalLenders` | int | Count of distinct lenders that have registered at least one methodology version |
| `methodologies.activeVersions` | int | Count of methodology versions currently in `ACTIVE` state across all lenders (excludes `SUPERSEDED` + `REVOKED`) |
### `attestations.*`
| Field | Type | Notes |
| ---------------------- | ---- | ------------------------------------------------------------------------------------- |
| `attestations.schemaA` | int | Count of on-chain `BrandAttestation` records (axis A — brand control) |
| `attestations.schemaB` | int | Count of on-chain `CreditRiskAttestation` records (axis B — credit underwriting) |
| `attestations.schemaC` | int | Count of on-chain `RepaymentHistoryAttestation` records (axis C — settlement history) |
| `attestations.schemaD` | int | Count of on-chain `CrossAttestation` records (axis D — peer trust) |
### `asOf`
| Field | Type | Notes |
| ------ | ------------------ | ----------------------------------------------------------------------- |
| `asOf` | ISO 8601 timestamp | Server time at which the rollup was computed. Use for freshness probes. |
## Privacy discipline
This endpoint is deliberately scoped to platform-wide aggregates. The following are **NOT** in the response:
* Individual lender names, IDs, or signing wallets
* Merchant identifiers (no `merchantId`, no shop slug, no wallet)
* Methodology hashes, document URLs, or version strings
* Service provider IDs or display names
* Any per-attestation data (`uid`, `issuerWallet`, `subject`, payload)
* Any PII (no email, no operator names, no admin actor IDs)
For per-row reads, use the dedicated public endpoints (each enforces its own redaction whitelist): [`/v2/lenders/:lenderId`](/api-reference/public/lender-registry), [`/v2/methodologies/:lenderId/:hash`](/api-reference/public/methodology-registry), [`/v2/service-provider-routing/recommend`](/api-reference/public/service-provider-routing), and the per-merchant attestation reads.
## When to use
* **Partner-facing dashboards** showing platform scale — "Droplinked has underwritten N merchants across M lenders" — without exposing the underlying merchant or lender list
* **Agent platform-scale signaling** — a consumer agent answering "how big is this trust fabric" before deciding to integrate
* **Freshness probes** — `asOf` lets a caller confirm the aggregate is computed recently (independent of any per-row read freshness)
* **Health checks** — non-zero counts across all four axes confirm the schema registry, reconciler, and on-chain pipeline are all live
## Caching
There's **no server-side cache** in v1 — every call hits the live aggregate. Given the aggregate nature of the response (counts only, no per-row data, no high-cardinality dimensions), clients **SHOULD** cache at a **60s+ TTL** to keep partner dashboards responsive without unnecessary load. The `asOf` field lets clients display freshness honestly even with stale-while-revalidate strategies.
## Curl example
```bash theme={null}
curl -s https://apiv3.droplinked.com/v2/trust-fabric/stats | jq .
```
## Related
* [Trust Fabric overview](/concepts/trust-fabric) — the 4-axis architecture this endpoint rolls up
* [Lender Registry Lookup](/api-reference/public/lender-registry) — per-`lenderId` profile read
* [Methodology Registry Lookup](/api-reference/public/methodology-registry) — per-`(lenderId, hash)` methodology read
* [Service-Provider Routing](/api-reference/public/service-provider-routing) — ranked WMS/3PL recommendations
## MCP tool
Consumer agents reach this endpoint via the `get_trust_fabric_stats` MCP tool (separate PR in droplinked-mcp, in flight) — so ChatGPT / Claude / OpenAI Agents SDK callers can query platform-scale aggregates without prior knowledge of the path scheme.
# Underwriting Signals Composite
Source: https://docs.droplinked.com/api-reference/public/underwriting-signals
One read that bundles Schema B latest-per-lender + Schema C merchant-wide rollup + CreditTier upgrade preview in a single envelope. Cuts 3-4 lender-agent round trips to 1.
`GET /v2/underwriting-signals/:merchantId` is the composite read for lender-agent / MCP `get_trust_dossier` / underwriting-portal flows resolving "should I underwrite this merchant + at what tier" in **one** round trip.
This endpoint is **public** and **read-only**. Aggregates data already available via the per-axis verifier endpoints (Schema B + Schema C + upgrade preview); the only new value is fewer round trips.
## Request
```
GET /v2/underwriting-signals/:merchantId
```
## Response (200)
```json theme={null}
{
"merchantId": "6a207db0d29923bffaa983ca",
"creditRisk": {
"hasAny": true,
"activeCount": 2,
"maxObservedTier": "T3",
"latestPerLender": [
{
"lenderId": "crediblex-uae",
"attestationUid": "0x...",
"creditTier": "T2",
"maxCreditLineUsdCents": 5000000,
"issuedAt": "2026-06-01T12:00:00Z",
"expiresAt": "2026-09-01T12:00:00Z",
"status": "ACTIVE",
"lenderCurrentStatus": "ACTIVE"
},
{
"lenderId": "valinor-vault",
"attestationUid": "0x...",
"creditTier": "T3",
"maxCreditLineUsdCents": 10000000,
"status": "ACTIVE",
"lenderCurrentStatus": "SUSPENDED"
}
]
},
"repaymentHistory": {
"hasAny": true,
"perLenderCount": 2,
"merchantWide": {
"totalSettlements": 17,
"totalOnTime": 13,
"totalLate": 3,
"totalDefaults": 1,
"trailingTwelveMonthDefaults": 1,
"mostRecentSettlementAt": "2026-06-05T12:00:00Z"
}
},
"upgradeEligibility": {
"observedTier": "T2",
"nextTierTarget": "T3",
"onTimeSettlementsNeeded": 8,
"blockingDefaultCount": 1
},
"summary": {
"anchorTier": "T3",
"totalActiveCreditLineUsdCents": 15000000,
"reliabilityScore": 76
}
}
```
## The `summary` envelope
`summary` is the **load-bearing decision input** for lender agents:
| Field | Meaning |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `anchorTier` | `max(observed-from-repayment, already-issued)` — lenders never under-price a merchant who has already proven a higher tier |
| `totalActiveCreditLineUsdCents` | Sum of `maxCreditLineUsdCents` across all ACTIVE Schema B attestations |
| `reliabilityScore` | `onTimeSettlements / totalSettlements * 100` (0-100); `null` when no settlement history yet (clean-slate semantics) |
## Watch `lenderCurrentStatus`
For each entry in `creditRisk.latestPerLender`, the `lenderCurrentStatus` field is the **last-observed registry status** of the issuing lender (refreshed every 6h by the reconciler). When `status: "ACTIVE"` but `lenderCurrentStatus` is `SUSPENDED` / `ARCHIVED` / `UNKNOWN`, the attestation is still on-chain ACTIVE but the issuer has been de-listed — verifiers decide whether to honor it.
## Discipline
* **Read-only**. No mints. No PII beyond per-axis verifiers.
* **`@Public()`** route — same trust-handshake stance as the per-axis verifier reads.
* **3 underlying queries parallelised** via `Promise.all` — single endpoint adds no latency over the slowest individual read.
## Related
* [Schema B Credit-Risk Read](/api-reference/public/attestation-credit-risk) — single-axis verifier
* [Schema C Repayment-History Read](/api-reference/public/attestation-repayment-history) — single-axis verifier
* [Upgrade Preview](/api-reference/public/upgrade-preview) — just the roadmap part
# Merchant Credit-Tier Upgrade Preview
Source: https://docs.droplinked.com/api-reference/public/upgrade-preview
Aspirational roadmap — given a merchantId, returns the tier the merchant qualifies for from their repayment history alone (observedTier) + the gap to the next ceiling + the gap to T3 (top ceiling).
`GET /v2/merchant/credit-tier/upgrade-preview/:merchantId` returns the merchant's **aspirational roadmap** to higher credit-tier ceilings. Used by merchant portals to surface "settle X more on-time and the next lender that underwrites you will start from a T2 ceiling."
**Aspirational, not a promise.** The actual issued tier on a Schema B attestation depends on the lender's base tier mapping (revenue + inventory + sales-efficiency signals from `CreditTierMappingService`). This endpoint shows the **floor** the merchant can earn through repayment performance; the **base** is set by lender underwriting.
## Request
```
GET /v2/merchant/credit-tier/upgrade-preview/:merchantId
```
## Response (200)
```json theme={null}
{
"merchantId": "6a207db0d29923bffaa983ca",
"signals": {
"totalSettledOnTime": 2,
"totalLate": 0,
"totalDefaults": 0,
"trailingTwelveMonthDefaults": 0,
"lenderCount": 1,
"mostRecentSettlementAt": "2026-06-08T12:00:00Z"
},
"observedTier": "T1",
"nextTierGap": {
"targetTier": "T2",
"onTimeSettlementsNeeded": 1,
"blockingDefaultCount": 0,
"guidance": "Settle 1 more on-time to reach T2 ceiling."
},
"topTierGap": {
"targetTier": "T3",
"onTimeSettlementsNeeded": 8,
"blockingDefaultCount": 0,
"guidance": "Settle 8 more on-time to reach T3 (top ceiling)."
}
}
```
## Tier ladder
| Tier | Threshold from repayment history | Blocks |
| ---- | ---------------------------------------- | ---------------------------------------- |
| `T1` | Default — every new merchant starts here | — |
| `T2` | 3+ on-time settlements | Lifetime defaults > 0 → blocked |
| `T3` | 10+ on-time settlements | Trailing-12-month defaults > 0 → blocked |
When the merchant reaches T3, both `nextTierGap` and `topTierGap` are `null`.
## Discipline
* **Read-only**. No mints. No reads of the lender base tier — pure "what's possible with repayment history alone" surface.
* **`@Public()`** — no PII beyond the merchant's own settlement counts.
## Related
* [Underwriting Signals](/api-reference/public/underwriting-signals) — composite read that bundles this preview with Schema B + C in one envelope
* [Schema C Repayment-History](/api-reference/public/attestation-repayment-history) — the on-chain record the preview is computed from
# Create a new shipping model
Source: https://docs.droplinked.com/api-reference/shipping/create-a-new-shipping-model
https://apiv3.droplinked.com/swagger/json post /shippings/v2
Route: ShippingService.create
# Delete a shipping model (only if no products are using it)
Source: https://docs.droplinked.com/api-reference/shipping/delete-a-shipping-model-only-if-no-products-are-using-it
https://apiv3.droplinked.com/swagger/json delete /shippings/v2/{id}
Route: ShippingService.deleteShippingModel
# Get a specific shipping model by ID
Source: https://docs.droplinked.com/api-reference/shipping/get-a-specific-shipping-model-by-id
https://apiv3.droplinked.com/swagger/json get /shippings/v2/{id}
Route: ShippingService.findOne
# Get all shipping models for the authenticated shop
Source: https://docs.droplinked.com/api-reference/shipping/get-all-shipping-models-for-the-authenticated-shop
https://apiv3.droplinked.com/swagger/json get /shippings/v2
Route: ShippingService.findAllByShopId
# Get available shipping providers
Source: https://docs.droplinked.com/api-reference/shipping/get-available-shipping-providers
https://apiv3.droplinked.com/swagger/json get /shippings/v2/providers
Route: ShippingService.getProviders
# Update an existing shipping model
Source: https://docs.droplinked.com/api-reference/shipping/update-an-existing-shipping-model
https://apiv3.droplinked.com/swagger/json put /shippings/v2/{id}
Route: ShippingService.updateShippingModel
# Add wallet to shop (JWT required)
Source: https://docs.droplinked.com/api-reference/shop/add-wallet-to-shop-jwt-required
https://apiv3.droplinked.com/swagger/json post /shops/v2/wallets
Route: ShopService.addWalletToShop
# Check if a shop URL is available
Source: https://docs.droplinked.com/api-reference/shop/check-if-a-shop-url-is-available
https://apiv3.droplinked.com/swagger/json get /shops/v2/check-url
Route: ShopService.checkShopUrlAvailability
# Create Circle wallet for shop (JWT required)
Source: https://docs.droplinked.com/api-reference/shop/create-circle-wallet-for-shop-jwt-required
https://apiv3.droplinked.com/swagger/json post /shops/v2/circle/wallet
Route: ShopService.createCircleWallet
# Delete shop (JWT required)
Source: https://docs.droplinked.com/api-reference/shop/delete-shop-jwt-required
https://apiv3.droplinked.com/swagger/json delete /shops/v2
Route: ShopService.deleteShop
# Get available login methods
Source: https://docs.droplinked.com/api-reference/shop/get-available-login-methods
https://apiv3.droplinked.com/swagger/json get /shops/v2/login-methods
Route: ShopService.getLoginMethods
# Get available payment methods (JWT required)
Source: https://docs.droplinked.com/api-reference/shop/get-available-payment-methods-jwt-required
https://apiv3.droplinked.com/swagger/json get /shops/v2/available/payment-methods
Route: ShopService.getAvailablePaymentMethodsV2
# Get public shop by ID
Source: https://docs.droplinked.com/api-reference/shop/get-public-shop-by-id
https://apiv3.droplinked.com/swagger/json get /shops/v2/public/{id}
Route: ShopService.getPublicShop
# Get public shop by name
Source: https://docs.droplinked.com/api-reference/shop/get-public-shop-by-name
https://apiv3.droplinked.com/swagger/json get /shops/v2/public/name/{name}
Route: ShopService.getPublicShopByName
# Get shop by domain
Source: https://docs.droplinked.com/api-reference/shop/get-shop-by-domain
https://apiv3.droplinked.com/swagger/json get /shops/v2/domain/{domain}
Route: ShopService.getShopByDomain
# Get shop by ID (JWT required)
Source: https://docs.droplinked.com/api-reference/shop/get-shop-by-id-jwt-required
https://apiv3.droplinked.com/swagger/json get /shops/v2
Route: ShopService.getShopById
# Get shop payment methods (JWT required)
Source: https://docs.droplinked.com/api-reference/shop/get-shop-payment-methods-jwt-required
https://apiv3.droplinked.com/swagger/json get /shops/v2/payment-methods
Route: ShopService.getPaymentMethods
# Get shop private key (JWT required)
Source: https://docs.droplinked.com/api-reference/shop/get-shop-private-key-jwt-required
https://apiv3.droplinked.com/swagger/json get /shops/v2/private-key
Route: ShopService.getShopById
# Setup shop (JWT required)
Source: https://docs.droplinked.com/api-reference/shop/setup-shop-jwt-required
https://apiv3.droplinked.com/swagger/json post /shops/v2/setup
Route: ShopService.setupShop
# Update shop payment methods (JWT required)
Source: https://docs.droplinked.com/api-reference/shop/update-shop-payment-methods-jwt-required
https://apiv3.droplinked.com/swagger/json put /shops/v2/payment-methods
Route: ShopService.updatePaymentMethods
# Update shop settings (JWT required)
Source: https://docs.droplinked.com/api-reference/shop/update-shop-settings-jwt-required
https://apiv3.droplinked.com/swagger/json patch /shops/v2
Route: ShopService.updateShop
# Get a specific SKU by ID with product information
Source: https://docs.droplinked.com/api-reference/sku/get-a-specific-sku-by-id-with-product-information
https://apiv3.droplinked.com/swagger/json get /sku-v2/{id}/with-product
Route: SkuV2Service.getSkuWithProductByIdWithShopValidation
# Get a specific SKU by ID with shop validation
Source: https://docs.droplinked.com/api-reference/sku/get-a-specific-sku-by-id-with-shop-validation
https://apiv3.droplinked.com/swagger/json get /sku-v2/{id}
Route: SkuV2Service.findSkuByIdWithShopValidation
# Get all SKUs for the authenticated shop
Source: https://docs.droplinked.com/api-reference/sku/get-all-skus-for-the-authenticated-shop
https://apiv3.droplinked.com/swagger/json get /sku-v2
Route: SkuV2Service.findSkusByShopId
# API Status
Source: https://docs.droplinked.com/api-reference/status
Real-time droplinked API health + incident history.
Live status badges below link directly to each service's `/health` endpoint.
For programmatic monitoring, hit those endpoints — they return JSON with
`status`, `timestamp`, and (for `apiv3`) `dbHost`.
## Current status
### Operational
Public API serving `200 OK`. Mongo cluster: `droplinked-prod`. The
`/health` endpoint returns `{ status, timestamp, dbHost, dbName, dbMode }`.
### Operational
3rdp integration services serving `200 OK`. PayPal partner-OAuth +
Stripe webhook signing both healthy. Returns the standard
`{ statusCode, status, data: { status, timestamp } }` envelope.
### Operational
Customer-facing shops + checkout flow live. 4-PSP aggregation:
Stripe / PayPal / Bonum / Crypto.
## Programmatic checks
```bash curl theme={null}
curl -s https://apiv3.droplinked.com/health | jq
```
```bash agent theme={null}
# For LLM agents — add docs.droplinked.com as an MCP server in your IDE
# (Cursor, VS Code, Claude Desktop, Windsurf). The contextual toolbar on
# any page in this site exposes one-click integration.
```
The full machine-readable OpenAPI spec is published at
`https://apiv3.droplinked.com/swagger/json`, and the Agentic Commerce
Protocol feed at `https://apiv3.droplinked.com/feed/acp.json`.
## Incident history
No active incidents. Subscribe to the status feed (coming soon) for
email notifications.
| Date | Service | Impact | Resolution |
| ---------- | ---------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| 2026-05-29 | apiv3 prod | KMS payout settlement degraded (≈12 errors/h on startup) | Resolved — env-shadow + DER decode fixes (droplinked-backend #1403 / #1417 / #1421) |
| 2026-05-28 | apiv3 prod | Stripe checkout 401 on canonical Stripe account | Resolved — wired `STRIPE_RECONCILE_KEY` through (droplinked-backend #1369) |
## SLA
This is a beta API. No formal SLA — best-effort `99%` uptime is targeted.
For production-critical integrations, embed health checks in your client
and alert when `/health` returns 5xx for 3+ consecutive checks.
# Authentication
Source: https://docs.droplinked.com/authentication
How to authenticate against the Droplinked API — public endpoints, merchant JWTs, and integration keys.
The Droplinked API exposes three access tiers. Pick the one that matches what you're building.
## Public endpoints (no auth)
Anything under a `/public/` path is open and unauthenticated — built for storefronts,
catalogs, and AI agents that read a merchant's published inventory.
```bash theme={null}
curl https://apiv3.droplinked.com/shops/v2/public/name/{shopName}
curl "https://apiv3.droplinked.com/product-v2/public/shop/{shopName}?page=1&limit=24"
```
Use these to discover shops and products without any credentials.
## Merchant / customer JWT (Bearer)
Authenticated actions (managing a shop, products, carts, orders) use a **Bearer JWT**.
```bash theme={null}
# 1. Obtain a token
curl -X POST https://apiv3.droplinked.com/merchant/login \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","password":"••••••"}'
# → { "data": { "accessToken": "" } }
# 2. Call an authenticated endpoint
curl https://apiv3.droplinked.com/shops/v2 \
-H "Authorization: Bearer "
```
Send the token as `Authorization: Bearer `. The scheme in the OpenAPI spec is
`bearer` (HTTP, JWT).
## Integration key (server-to-server)
Partner/integration services authenticate to the Integration Services layer with an
`integration-api-key` header (a `3RPD_…` key issued to your integration). This is for
backend-to-backend calls, not browser or agent clients.
```bash theme={null}
curl https://service.droplinked.com/locations/countries \
-H "integration-api-key: 3RPD_…"
```
Base URLs for production and development.
# Droplinked vs Shopify
Source: https://docs.droplinked.com/concepts/droplinked-vs-shopify
How droplinked's commerce platform differs from Shopify — same product-types and PSPs, but with on-chain trust fabric, multi-PSP routing, agentic-commerce surfaces, and built-in lender ecosystem.
Shopify is the gold standard for SMB commerce. The visual store editor, the massive
theme library, the app store, the brand recognition, the funded support team — for a
merchant who needs a turnkey storefront and a mature SaaS workflow, it's hard to beat.
Droplinked does not try to.
Droplinked starts from a different assumption: that the merchant's **identity**,
**inventory**, and **underwriting** can be cryptographically verifiable; that
**distribution** should reach AI shopping agents in addition to human browsers; and
that **lending capital** should be sourced from licensed DeFi protocols and vaults
rather than a single processor's balance sheet. Same product types. Same PSPs.
Different trust posture.
This page is for two audiences. If you're a merchant evaluating commerce platforms,
it lays out the honest tradeoff. If you're a partnership lead evaluating droplinked
as an integration target — a lender, a 3PL, an agent surface — it surfaces the
differentiators that make droplinked a distinct integration surface from Shopify.
## What's the same
Both platforms cover the baseline a modern commerce merchant expects:
| Capability | Shopify | Droplinked |
| ------------------------------------------------------ | -------------------- | ------------------------------------------------------ |
| Physical / digital / POD product types | Yes | Yes |
| Multi-PSP support (Stripe, PayPal, regional acquirers) | Yes | Yes (5+ wired in: Stripe, PayPal, Telr, Bonum, PayMob) |
| Theme templates + storefront customization | Yes (massive) | Yes (focused set) |
| Order management + shipping (EasyPost / carriers) | Yes | Yes ([EasyPost](/guides/integrations/easypost)) |
| Multi-currency / multi-language | Yes | Yes |
| Analytics dashboard | Yes | Yes |
| Merchant KYB onboarding | Yes | Yes (chain-anchored — see Schema A) |
| Print-on-demand integrations | Yes (Printful, etc.) | Yes ([Printful](/guides/integrations/printful)) |
| AI-assisted content (titles, descriptions, banners) | Yes (Shopify Magic) | Yes ([AI endpoints](/api-reference/ai/overview)) |
| Public catalog APIs | Yes | Yes (`/product-v2/public/shop/{name}`) |
If your evaluation stops at this row, both platforms can carry the same catalog.
The differentiators sit one layer up.
## What's uniquely droplinked
Schema A `BrandAttestation` pins your brand slug cryptographically to a signing
wallet on the [Ethereum Attestation Service](https://attest.org). Impostors
can't claim your slug; verifiers can independently audit your brand identity
on-chain. See [Trust Fabric](/concepts/trust-fabric).
`LenderRegistry` + `ServiceProviderRegistry` + `MethodologyRegistry` anchor a
4-axis trust fabric (brand / credit-risk / repayment-history / peer-trust).
Compare to Stripe Capital: droplinked routes you to a **licensed lender**
publishing an auditable methodology, not just a processor's internal credit decision.
A per-merchant MCP server (`mcp.droplinked.com/{shopSlug}/...`) and a
[Stripe ACP feed](/agentic/acp-feed) mean ChatGPT, Claude, Cursor, and the
OpenAI Agents SDK can discover and transact against your catalog without you
building integrations. See [Agentic Commerce](/agentic/overview).
Five PSPs from one wire — Stripe + PayPal as Tier-1 always-eligible, plus
Telr / Bonum / PayMob for corridor-aware routing across GCC, MENA, Mongolia,
Egypt. The resolver picks the optimal authorized PSP per transaction; you
don't pick per-checkout.
Full affiliate dashboard, commission tracking, payouts, and a discovery
marketplace — without an app-store install. Conversions can settle through
x402 micropayments and a 70/20/10 split that rewards the surface that closed
the sale.
NFT-gated drops, stablecoin settlement, and POD inventory are first-class
catalog citizens — not bolt-on apps you wire together yourself.
## What Shopify still does better (honest)
If we don't surface this list, the page isn't useful. Shopify has real advantages
that haven't gone away:
* **Drag-and-drop visual store editor** with a theme marketplace measured in
thousands of options and a long tail of agencies that can customize them.
* **App store** — thousands of plugins for loyalty, reviews, SMS marketing,
subscriptions, returns, upsell, etc. Most niches are a one-click install away.
* **Brand recognition + a funded support team.** Merchants know what Shopify is;
procurement and finance teams have heard of it.
* **Mature SaaS workflow** — abandoned-cart recovery, email campaign builder,
returns flow, reviews UX, customer-profile CRM. Years of iteration baked in.
* **POS hardware** for in-person retail (card reader, in-store register, unified
inventory across online + offline).
Droplinked is younger on every one of these dimensions. We say so on purpose.
## Pricing posture (qualitative)
Shopify charges roughly **$39–$2,000 / month + 2.4–2.9% per transaction** depending
on tier (Basic / Shopify / Advanced / Plus), plus per-app subscriptions on top.
Droplinked's pricing model is operator-controlled and tier-based. Specifics live on
[droplinked.com](https://droplinked.com) — please contact sales for current tier
pricing rather than trusting a number quoted in a docs page that may drift. The
mental model: droplinked tries to capture less per-transaction friction and more
of its margin from the trust-fabric + agentic-distribution side of the stack.
## When to choose droplinked
* You want your **brand cryptographically verifiable on-chain** — anti-impersonation,
regulator-auditable, slug-pinned to a signing wallet (Schema A
`BrandAttestation`).
* You're **underserved by Stripe Capital** and want access to licensed DeFi lenders,
vaults, and treasuries underwriting against your real cash flow. The
`LenderRegistry` carries jurisdiction-aware routing — `?jurisdiction=AE` returns
ACTIVE lenders in your corridor with `GLOBAL` fallback.
* You want **agent-shoppable distribution** — your catalog discoverable inside
ChatGPT, Claude, Cursor without each integration being a separate BD cycle.
Every shop gets a per-merchant MCP surface at `mcp.droplinked.com/{shopSlug}/...`
with no extra wiring.
* You **operate across multi-PSP corridors** — GCC, MENA, Mongolia, Egypt — where a
single global processor leaves money on the table or fails outright. The
multi-PSP resolver routes to the optimal authorized acquirer per transaction.
* You want a **built-in affiliate network** rather than stitching together a
third-party app and a payout reconciler. Conversions can settle through x402
micropayments + a 70/20/10 split that rewards the surface (publisher, agent,
storefront) that closed the sale.
* You sell **digital products, NFTs, or POD items** as first-class catalog entries
rather than via plugins that papered over a physical-only data model.
* You want **a forensic audit trail you can hand to a regulator or a new payment
partner** — every status change, attestation mint, and repayment event is
preserved on-chain. See [Forensic Chain Workflow](/concepts/forensic-chain).
## When to choose Shopify (or stay on Shopify)
Also honest:
* You need a **massive visual theme marketplace** and an agency ecosystem fluent in
customizing it.
* You need a **mature app ecosystem** — loyalty programs, reviews infrastructure,
SMS marketing, subscriptions, returns workflows.
* You're a **brick-and-mortar retailer** needing POS hardware and a unified
inventory layer across online + in-person.
* You've **already built your tech stack on Shopify Plus** and the migration cost
outweighs the trust-fabric + agentic-distribution upside today.
## Hybrid posture
**Droplinked is additive distribution.** You don't have to replatform off Shopify
to use it. Keep your Shopify storefront and connect your catalog to droplinked for
the agent-shoppable + on-chain attestation layer. See
[Connect your store](/agentic/connect-your-store) — the goal is additive reach,
not migration.
The cleanest hybrid pattern in practice:
1. Keep Shopify as your operational backend — themes, app store apps, POS,
abandoned-cart flow, marketing email, accounting integrations.
2. Connect the same catalog to droplinked — your products appear in the ACP feed
and in a per-merchant MCP surface ChatGPT / Claude / Cursor can call.
3. Optionally pull a Schema A brand attestation on the droplinked side so verifier
agents can confirm the brand identity behind both your Shopify storefront and
your droplinked surface.
4. Optionally apply for a verifiable credit-line via the `LenderRegistry` — even
if all settlement still happens through your Shopify-Stripe stack today.
That posture lets you keep the Shopify SaaS workflow you're already paying for and
layer droplinked on top as a distribution + capital surface, not a replacement.
## Migration intent
If you do want to bring the catalog over wholesale, see
[Connect your store](/agentic/connect-your-store) for the connect-an-existing-store
surface. Direct Shopify → droplinked product import (bulk SKU + image pull from a
Shopify admin token) is on the roadmap; for now the connect path treats your
Shopify store as the source of truth and projects it into the droplinked agentic
layer.
## Architecture comparison
| Dimension | Shopify | Droplinked |
| ----------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| **PSP routing** | Shopify Payments (Stripe-backed default) with manual configuration of alternates | Multi-PSP backbone with **corridor-aware routing** across 5+ acquirers |
| **Lending capital** | Shopify Capital — Stripe balance-sheet-funded, processor-internal decision | `LenderRegistry` trinity — licensed DeFi protocols, vaults, and treasuries, with a published methodology hash anchored on-chain |
| **Brand identity** | Shopify domain + brand assets in a SaaS account | Schema A `BrandAttestation` — brand slug pinned to a signing wallet on-chain |
| **Underwriting transparency** | Internal model; merchants see the outcome, not the criteria | `MethodologyRegistry` publishes the per-lender methodology hash linked from every Schema B attestation |
| **Agent reach** | Shopify Magic Search + emerging agentic surfaces | Per-merchant MCP server + [ACP feed](/agentic/acp-feed) consumed by ChatGPT / Claude / Cursor today |
| **Attestation audit** | Shopify dashboard logs (operator-internal) | Trinity-wide append-only audit log + public timeline endpoints (lender-history, methodology-timeline) |
| **Affiliate layer** | Third-party apps (Refersion, GoAffPro, etc.) | First-class — dashboard, commission tracking, x402 settlement, 70/20/10 split |
| **Service-provider routing** | Manual partner directory | `ServiceProviderRegistry` + `/v2/service-provider-routing/recommend` — track-record-ranked |
## The bridge-the-gap angle
Droplinked sits between **DeFi capital** (yield-seeking vaults, protocol treasuries,
on-chain lending desks) and **real commerce** (merchants generating cash flow on
physical, digital, and POD catalogs). Shopify Capital is funded from Stripe's
balance sheet and quoted from an internal credit model. Droplinked surfaces capital
from licensed lenders publishing their methodology on-chain, with repayment streams
back to the lender wallet anchored in append-only Schema C attestations.
It's a different posture, not a better processor. If you want capital from a
balance-sheet you can independently audit, droplinked is the surface that exposes
it. If you want the Stripe balance sheet wrapped in a SaaS UX, Shopify Capital is
exactly that.
The same posture extends to the **service-provider side**: 3PLs, WMS partners, and
fulfillment operators can plug in as Schema D peer-trust issuers, and merchants
discover them through `/v2/service-provider-routing/recommend` ranked by track
record rather than paid placement. The Shopify analogue is the partner directory
plus per-app reviews; droplinked's analogue is a track-record-ranked routing
endpoint anchored in an append-only audit log.
## Common questions from evaluating merchants
**"Do I have to use crypto?"** No. Card / PayPal / regional acquirer settlement
is first-class. Stablecoin settlement is one path, not the only one. Merchants who
never touch a wallet still get every droplinked benefit — multi-PSP routing,
agent distribution, the affiliate network — with fiat payouts.
**"Do my customers have to know about any of this?"** No. The customer sees a
checkout page with their familiar payment methods (card, PayPal, BNPL, Apple Pay,
crypto). The PSP under the card category is invisible — that's a routing decision
droplinked makes server-side based on the merchant's authorized PSPs.
**"Do I lose the Shopify themes I already built?"** Only if you replatform.
The [Connect your store](/agentic/connect-your-store) path keeps your Shopify
storefront live and projects your catalog into droplinked's agentic + attestation
layers in parallel. Your themes don't move.
**"Is the trust fabric live today?"** Yes — currently on Base Sepolia testnet for
the full 4-axis Schema v2 (`A` / `B` / `C` / `D`) and the registry trinity
(`LenderRegistry` / `ServiceProviderRegistry` / `MethodologyRegistry`). Mainnet is
gated on the KMS-backed signer migration. Every `/v2/attestations/*` endpoint
returns the active chain in its response so verifiers can confirm receipts on the
right [easscan](https://easscan.org) domain.
**"What does the integration look like in code?"** Read APIs are the same shape
as any commerce REST surface — `GET /shops/v2/public/name/{name}`,
`GET /product-v2/public/shop/{name}`. Trust-fabric reads are
`GET /v2/attestations/...` and the composite
[`GET /v2/underwriting-signals/:merchantId`](/api-reference/public/underwriting-signals).
Agents reach you via [MCP](/agentic/mcp-server) and [ACP](/agentic/acp-feed).
## What's coming
We try not to over-promise. The honest roadmap for the next two quarters, as it
affects parity with Shopify's mature SaaS workflow:
* **Discount + promotion engine** — in flight.
* **Abandoned-cart recovery** — scoped, not built.
* **Returns UI** — scoped, not built.
* **Email campaign builder** — outreach + transactional are wired; merchant
campaign UX is roadmap.
* **Direct Shopify product import** — roadmap.
If parity on a specific workflow is load-bearing for your evaluation, it's worth a
conversation with the team rather than trusting a docs roadmap snapshot.
## Related
* [Platform model](/concepts/platform-model) — the core Droplinked objects (Shop,
Product, Cart, Order).
* [Trust Fabric](/concepts/trust-fabric) — the 4-axis on-chain attestation layer.
* [Merchants on Droplinked](/concepts/for-merchants) — the merchant-side narrative.
* [DeFi Lender Onboarding](/concepts/for-defi-lenders) — how lenders plug in.
* [Service Providers on Droplinked](/concepts/for-service-providers) — how 3PLs
and WMS partners plug in.
* [Agentic Commerce](/agentic/overview) — MCP + ACP feed + x402 settlement.
* [Connect your store](/agentic/connect-your-store) — keep Shopify, add droplinked
for agentic distribution.
# DeFi Lender Onboarding
Source: https://docs.droplinked.com/concepts/for-defi-lenders
Plug a DeFi vault, treasury, or lending protocol into droplinked's commerce-yield bridge. Issue credit-risk attestations, pin underwriting methodology on-chain, and receive verifiable repayment streams from real merchant sales.
Droplinked sits between **on-chain capital** and **real-world commerce**. If you run
a DeFi vault, a protocol treasury, or a credit-line lending protocol with stable
assets you'd like to deploy into yield-bearing real-economy positions, this page is
the canonical onboarding narrative.
The bridge is three layers:
1. **DeFi capital** — your vault / treasury sits on idle stablecoin or asset reserves
looking for diversified, non-synthetic yield.
2. **The trust fabric** — droplinked's on-chain attestation layer (EAS Schema v2 — see
[Trust Fabric](/concepts/trust-fabric)) lets you publish your underwriting
methodology in a cryptographically verifiable way and issue per-merchant
credit-risk attestations regulators can independently audit.
3. **Real-world commerce** — droplinked merchants (physical goods, digital, POD,
in-store retail) generating actual sales — actual cash flow — and therefore actual
repayments back to your wallet.
The yield is **real repayment streams from real merchant sales**, with full
chain-anchored auditability. Not synthetic yield. Not rehypothecated collateral. Not
a wrapped-up money-market position with a TradFi counterparty risk you can't see.
## Architecture at a glance
```mermaid theme={null}
flowchart TD
A["DeFi vault / treasury (your on-chain wallet)"] --> B["LenderRegistry (operator-curated)"]
B --> C["MethodologyRegistry (per-lender methodology hash)"]
C --> D["Schema B CreditRiskAttestation (per merchant)"]
D --> E["Merchant sales (physical / digital / POD)"]
E --> F["Schema C RepaymentHistoryAttestation (append-only settlement)"]
F --> G["Repayment to lender wallet"]
G --> A
```
Every arrow above is either an on-chain attestation, a registry mutation captured in
an append-only audit log, or a verifiable settlement event. A regulator —
FSRA, SCA, or your jurisdiction's equivalent — can reconstruct the full history of
any underwriting decision you made and any repayment you received without trusting
droplinked.
## Why droplinked?
* **Real-world yield, not synthetic.** Every repayment is anchored to a real
merchant order on a real PSP (Stripe, PayPal, Telr, Bonum, PayMob). No
rehypothecation, no second-order DeFi exposure.
* **Cryptographic verifiability.** Every credit-risk decision you issue is an
on-chain attestation (Schema B) pinning a SHA-256 of the underwriting methodology
you published. Verifiers re-hash and refuse on divergence.
* **Per-merchant credit-risk granularity.** Underwrite one merchant or one thousand;
each gets its own Schema B attestation with its own `creditTier` +
`maxCreditLineUsdCents` + `expiresAt` lifecycle. No portfolio-level smearing.
* **Methodology hash pinning.** You can't silently swap underwriting basis. If you
publish v2 of your scorecard, old attestations stay pinned to the v1 hash they
cited at mint time; new mints cite v2. Every divergence is auditable.
* **Audit trail for regulators.** Every registry mutation — lender registration,
status change, methodology supersession, methodology revocation — lands in an
append-only audit log preserved by droplinked. Regulators querying the operator
console can reconstruct any state at any past timestamp.
* **Routing surfaces.** Verified merchants in matching jurisdictions can discover
you via [`GET /v2/lender-routing/recommend`](/api-reference/public/lender-routing)
— exact-jurisdiction first, `GLOBAL` fallback. Your jurisdiction is your
first-mover moat.
* **MCP surface for consumer agents.** ChatGPT / Claude / Cursor / OpenAI Agents
SDK can discover and recommend you to onboarding merchants directly via the
[droplinked-mcp server](/agentic/mcp-server). No partner BD required.
## The 4-step onboarding flow
The operator console gates lender registration (SUPER\_ADMIN). Provide the
legal entity name, archetype, jurisdiction, signing wallet placeholder, and
regulator reference. Status starts at `PENDING_KYB`.
```bash theme={null}
curl -X POST https://apiv3.droplinked.com/admin/lenders \
-H "Authorization: Bearer $SUPER_ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{
"lenderId": "your-lender-slug",
"displayName": "Your Lender (Jurisdiction)",
"archetype": "defi-vault",
"jurisdiction": "GLOBAL",
"signingWallet": "0x0000000000000000000000000000000000000000",
"regulatorReference": null
}'
```
**What changes**: a new row in `LenderRegistry` (status `PENDING_KYB`) +
a `LENDER_REGISTERED` event in `lender-audit-log` (preserved forever).
Upload your methodology document (PDF or markdown) to a stable URL — your
own domain, IPFS, Arweave — and register the SHA-256 hash with droplinked.
```bash theme={null}
HASH=$(shasum -a 256 ./methodology-v1.pdf | awk '{print $1}')
curl -X POST https://apiv3.droplinked.com/admin/methodologies \
-H "Authorization: Bearer $SUPER_ADMIN_JWT" \
-H "Content-Type: application/json" \
-d "{
\"lenderId\": \"your-lender-slug\",
\"version\": \"1.0.0\",
\"methodologyHash\": \"$HASH\",
\"documentUrl\": \"https://yourdomain.example/methodology-v1.pdf\",
\"displayName\": \"Your Lender — Working-Capital Methodology v1.0\"
}"
```
**What changes**: a new row in `MethodologyRegistry` (status `ACTIVE`) +
a `METHODOLOGY_REGISTERED` event in `methodology-audit-log`. From this
moment on, every Schema B mint by your lenderId cites this hash.
Once your KYB is verified by droplinked, the operator flips your status
to `ACTIVE`. Only ACTIVE lenders can mint Schema B attestations.
```bash theme={null}
curl -X POST https://apiv3.droplinked.com/admin/lenders/your-lender-slug/status \
-H "Authorization: Bearer $SUPER_ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{ "newStatus": "ACTIVE", "reason": "KYB verified" }'
```
**What changes**: `LenderRegistry.status` flips to `ACTIVE` + a
`LENDER_STATUS_CHANGED` event lands in `lender-audit-log`. The
`EasIssuer.isActive` gate now lets your wallet sign Schema B attestations.
For each merchant you underwrite, issue a Schema B `CreditRiskAttestation`
pinning your methodology hash. The on-chain UID becomes the verifiable
primary key.
```bash theme={null}
curl -X POST https://apiv3.droplinked.com/v2/attestations/credit-risk \
-H "Authorization: Bearer $LENDER_JWT" \
-H "Content-Type: application/json" \
-d '{
"merchantId": "6a207db0d29923bffaa983ca",
"lenderId": "your-lender-slug",
"creditTier": "T2",
"maxCreditLineUsdCents": 5000000,
"methodologyHash": "0xabc123...",
"expiresAt": "2026-12-31T23:59:59Z"
}'
```
**What changes**: a new on-chain attestation on Base Sepolia (mainnet
pending KMS migration). Verifiers can pull it via
[`GET /v2/attestations/credit-risk/:merchantId`](/concepts/trust-fabric)
and walk the [forensic chain](/concepts/forensic-chain) back to your
methodology + your registry profile.
## What verifiers see
Once registered + ACTIVE, your offering is exposed via these public read endpoints —
no JWT, no IP-allowlist:
| Endpoint | Surfaces |
| ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| [`GET /v2/lenders/:lenderId`](/api-reference/public/lender-registry) | Your public profile + signing wallet (verifiers cross-check vs the on-chain `issuerWallet`) |
| [`GET /v2/lenders/:lenderId/timeline`](/api-reference/public/lender-registry) | Your lifecycle history (registered, status changes, metadata updates — operator-only fields redacted) |
| [`GET /v2/lenders`](/api-reference/public/lender-registry) | Your discoverable presence in the public lender list |
| [`GET /v2/methodologies/:lenderId/active`](/api-reference/public/methodology-registry) | The current methodology in force for new Schema B mints |
| [`GET /v2/methodologies/:lenderId/:hash`](/api-reference/public/methodology-registry) | Hash lookup — verifier downloads `documentUrl` + re-hashes for integrity |
| [`GET /v2/methodologies/:lenderId/versions`](/api-reference/public/methodology-registry) | Your full methodology lineage (ACTIVE + SUPERSEDED + REVOKED, newest-first) |
| [`GET /v2/methodologies/:lenderId/:hash/timeline`](/api-reference/public/methodology-registry) | Per-version lifecycle (registered → superseded → revoked, redacted) |
| [`GET /v2/trust-fabric/stats`](/api-reference/public/trust-fabric-stats) | Your jurisdiction's aggregate weight in the trust fabric |
## Agent surface (MCP)
Consumer agents — ChatGPT, Claude, Cursor, OpenAI Agents SDK — call these tools on
the [droplinked-mcp server](/agentic/mcp-server) and surface you to onboarding
merchants automatically:
| Tool | How this surfaces your offering |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| [`verify_lender`](/agentic/lender-trinity-mcp-tools) | Agent resolves a `lenderId` from a Schema B attestation back to your human-readable profile |
| [`recommend_lender`](/agentic/lender-trinity-mcp-tools) | Agent answering "which lenders should this merchant approach?" — you appear ranked by jurisdiction-match + track record |
| [`verify_methodology`](/agentic/lender-trinity-mcp-tools) | Agent confirms an attestation cites a real, published methodology you registered |
| [`get_lender_history`](/agentic/lender-trinity-mcp-tools) | Agent walks your lifecycle to confirm you were ACTIVE at attestation mint time |
| [`get_methodology_timeline`](/agentic/lender-trinity-mcp-tools) | Agent walks a methodology's lifecycle to confirm it wasn't already revoked / superseded at mint time |
| [`get_methodology_versions`](/agentic/lender-trinity-mcp-tools) | Agent walks your full methodology lineage when no specific hash is in hand |
| [`get_trust_fabric_stats`](/agentic/lender-trinity-mcp-tools) | Agent gauges platform scale (and your jurisdiction's share) before recommending you |
## Audit-trail commitment
**Every registry mutation produces an append-only audit-log entry preserved by
droplinked**. The two logs (`lender-audit-log` + `methodology-audit-log`) are
write-only at the operator console — there is no delete path. Regulators querying
the operator console can reconstruct any state at any past timestamp. Methodology
document tampering is provably detectable because the SHA-256 hash is on-chain at
mint time: a verifier downloads `documentUrl`, re-hashes, and refuses on divergence.
## Status semantics
### LenderRegistry status
| Status | Meaning | Can mint Schema B? | Verifier policy |
| ------------- | ------------------------------------------ | ------------------ | ------------------------------- |
| `PENDING_KYB` | Registered but droplinked KYB not complete | No | Refuse |
| `ACTIVE` | KYB verified, fully operational | Yes | Honor |
| `SUSPENDED` | Temporarily de-listed (operator decision) | No new mints | Honor existing per policy, flag |
| `ARCHIVED` | Permanently de-listed | No | Honor existing per policy, flag |
### MethodologyRegistry status
| Status | Meaning | New mints cite this? | Verifier policy |
| ------------ | -------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------- |
| `ACTIVE` | Currently in force | Yes | Honor |
| `SUPERSEDED` | Lender published a newer version | No | Honor historical attestations issued before `supersededAt` (methodology was in force at the time) |
| `REVOKED` | Operator pulled the methodology | No | Red flag — refuse pending review |
The reconciler cron (every 6h) mirrors `LenderRegistry.status` onto every ACTIVE
Schema B attestation via the `lenderCurrentStatus` + `lenderCurrentStatusAt`
fields. The reconciler **never auto-revokes** an attestation; it only mirrors. See
[Trust Fabric — Issuer-state mirroring](/concepts/trust-fabric#issuer-state-mirroring)
for the full semantics.
## Reference dashboard
You can embed the trust-fabric scale + your own activity in your own internal portal
or partner-facing UI. See the
[Trust-Fabric Dashboard Template](/guides/integrations/trust-fabric-dashboard-template)
for a drop-in copy-pastable HTML + JavaScript implementation (vanilla JS and React
flavors) that polls `/v2/trust-fabric/stats` + the public lender list.
## Verifier integrity recipe
The canonical 3-step verifier flow any regulator or counter-party will run:
1. **Read the on-chain Schema B attestation** —
`GET /v2/attestations/credit-risk/:merchantId`. Pin `attestationUid`,
`lenderId`, `issuerWallet`, `methodologyHash`, `occurredAt`.
2. **Resolve the methodology hash** —
`GET /v2/methodologies/:lenderId/:methodologyHash`. Pull `documentUrl` +
`status` + `effectiveAt`.
3. **Re-hash the document** —
`curl -sLo /tmp/methodology.pdf $DOC_URL && shasum -a 256 /tmp/methodology.pdf`.
Compare against the on-chain `methodologyHash`. Any divergence is methodology
tampering and the attestation must be refused.
See [Forensic Chain Workflow](/concepts/forensic-chain) for the full end-to-end
walkthrough including lender + repayment-history cross-checks.
## Operator gates
**Actual onboarding is operator-gated (SUPER\_ADMIN).** The endpoints in the
4-step flow above are admin-only — droplinked's operator console executes them on
your behalf after verifying your KYB packet.
To start, contact `support@droplinked.com` with:
* Legal entity name + corporate jurisdiction
* Regulator reference (FSRA / SCA / equivalent license number, or N/A if DeFi vault)
* Methodology document URL (https\:// or ipfs\://) — must be stable + immutable
* Signing wallet address (EVM, 20-byte) — this is the wallet that will sign Schema B
attestations and the address you'll receive repayments at
* Archetype: `fsra-licensed` / `defi-vault` / `generic`
* Jurisdiction: ISO 3166-1 alpha-2 (e.g. `AE`, `SG`, `US`) or `GLOBAL` for
unrestricted DeFi vaults
Mainnet is currently gated on the KMS-backed signer migration. Onboarding can
proceed against Base Sepolia testnet today; mainnet flip is operator-coordinated.
## Related
* [Trust Fabric (EAS Schema v2)](/concepts/trust-fabric) — 4-axis architecture overview
* [Forensic Chain Workflow](/concepts/forensic-chain) — end-to-end verifier walkthrough
* [Lender Registry Lookup](/api-reference/public/lender-registry) — `/v2/lenders/*` reference
* [Methodology Registry Lookup](/api-reference/public/methodology-registry) — `/v2/methodologies/*` reference
* [Trust-Fabric Stats](/api-reference/public/trust-fabric-stats) — `/v2/trust-fabric/stats` reference
* [Lender Trinity MCP Tools](/agentic/lender-trinity-mcp-tools) — agent-callable wrappers
* [Trust-Fabric Dashboard Template](/guides/integrations/trust-fabric-dashboard-template) — drop-in HTML + JS reference
# Merchants on Droplinked
Source: https://docs.droplinked.com/concepts/for-merchants
Get verifiable credit lines, multi-PSP checkout, agent-shoppable distribution, and a chain-anchored audit trail — without re-platforming.
Droplinked is what you reach for when a Shopify-plus-Stripe stack stops being enough.
You already have a catalog. You already have customers. What you don't have —
unless you bolt it on yourself, one vendor at a time — is access to **real
verifiable credit**, a **multi-PSP backbone** that works across every corridor your
buyers actually shop from, an **agent-discoverable surface** for the ChatGPT /
Claude / Cursor shopping wave, and a **chain-anchored audit trail** you can hand to
a regulator or a new payment partner without three weeks of forensic work.
That's the merchant side of the trust fabric. Lenders plug in from one side
(see [DeFi Lender Onboarding](/concepts/for-defi-lenders)); merchants plug in from
the other. The two meet in the middle on real settlement against real cash flow.
This page is the practical hands-on narrative: what you gain, how onboarding flows,
what stays private, and what you explicitly **don't** have to build yourself.
## What you gain
Licensed lenders (FSRA, SCA, DeFi vaults) issue per-merchant Schema B
credit-risk attestations pinned to a published underwriting methodology.
Collateral-light, regulator-auditable.
One integration → checkout routes through Stripe, PayPal, Telr, Bonum, or
PayMob depending on the buyer's corridor. You don't pick — the resolver does.
Your catalog flows to the ACP feed and a per-merchant MCP endpoint. ChatGPT,
Claude, Cursor, and the OpenAI Agents SDK can discover and transact against
your storefront with zero extra wiring.
Operator + verifiers see the attestation envelope. PII (legal entity name,
banking info, contact details) never leaves the operator console. Public
verifier endpoints return only chain-anchored claims.
Schema C repayment-history attestations accumulate every settlement event.
Tier rises as performance accrues. `GET /v2/upgrade-preview` projects
points-to-next-tier in real time.
Every status change, attestation mint, and repayment event is preserved. You
can prove your good standing to a regulator or a new partner without
excavating ticket archives.
## Architecture at a glance
```mermaid theme={null}
flowchart TD
A["Your catalog (physical / digital / POD / in-store)"] --> B["droplinked"]
B --> C["ACP feed (apiv3.droplinked.com/feed/acp.json)"]
B --> D["Per-merchant MCP (mcp.droplinked.com/{slug}/...)"]
C --> E["Agent shopping surfaces (ChatGPT / Claude / Cursor)"]
D --> E
B --> F["Schema B credit-risk attestation (minted by licensed lender)"]
F --> G["Checkout settlement via PSP backbone (Stripe / PayPal / Telr / Bonum / PayMob)"]
G --> H["Schema C repayment-history (append-only)"]
H --> I["Tier upgrade unlocked (Schema D peer-trust score rises)"]
I --> F
```
Each arrow is either an on-chain attestation, a registry mutation captured in an
append-only audit log, or a verifiable settlement event. You — and any regulator
or new partner — can reconstruct the full trajectory without trusting droplinked.
## Onboarding flow
Standard droplinked platform onboarding at
[droplinked.com](https://droplinked.com). Same flow you'd expect from a
Shopify-style platform: shop slug, branding, product import, payout wallet
or bank details. This is the existing platform layer — no trust-fabric work
yet.
Confirm your catalog is flowing into the agent-discoverable surfaces.
```bash theme={null}
# ACP feed entry for your shop
curl -s "https://apiv3.droplinked.com/feed/acp.json" \
| jq '.[] | select(.shop_slug == "your-shop-slug")'
# Per-merchant MCP manifest
curl -s "https://mcp.droplinked.com/your-shop-slug/manifest.json"
```
If you run a custom storefront, add the
[`` advertisement tag](/agentic/storefront-mcp-discovery)
to your HTML head so consumer agents auto-discover your MCP endpoint.
From the trust-fabric card in your dashboard, click **Request brand
attestation**. The request walks
`PENDING → APPROVED → MINTED` (or `→ REJECTED`) — operator-reviewed,
then the orchestrator mints a Schema A `BrandAttestation` on-chain.
No further action needed; the card polls the status endpoint and
auto-advances.
Full walkthrough — including the MCP-tool path for AI-agent onboarding
flows and the easscan verifier link — in
[Brand attestation: request → mint → verify](/guides/trust-fabric/brand-attestation-lifecycle).
Submit a lending application. droplinked's
[`/v2/lender-routing/recommend`](/api-reference/public/lender-routing)
surface picks the right-jurisdiction lender for your corridor — exact match
first, `GLOBAL` fallback.
```bash theme={null}
curl -s "https://apiv3.droplinked.com/v2/lender-routing/recommend?jurisdiction=AE" \
| jq '.recommended[]'
```
The lender underwrites you against their published methodology, mints a
Schema B `CreditRiskAttestation` pinning the methodology hash, and the
`EasIssuer.isActive` gate enforces that only currently-ACTIVE lenders can
sign.
Sales settle through the
[checkout-intent-resolver](/api-reference/public/checkout-intent-resolver) —
one of the 5 PSPs handles the authorization based on your buyer's corridor.
Repayments against your credit line are recorded as Schema C
`RepaymentHistoryAttestation` events (append-only). Tier rises as your
repayment record accrues — query
[`GET /v2/upgrade-preview`](/api-reference/public/upgrade-preview) any time
for the projection.
## What verifiers see about you
The public verifier surface **never** exposes operator-only fields. A regulator
querying [`GET /v2/lenders/:lenderId/timeline`](/api-reference/public/lender-registry)
about a lender that underwrote you sees only event types + status diffs — never
the actor, the reason text, or any PII. The same redaction policy applies to
methodology timeline endpoints and to the public credit-risk attestation
endpoint.
Walk the full verifier sequence in
[Forensic Chain Workflow](/concepts/forensic-chain) to see exactly what fields a
counter-party can re-derive from chain + public reads alone.
## Multi-PSP from one wire
| PSP | Coverage | Tier |
| ------ | ----------------------------------------- | ---- |
| Stripe | Global backbone — every merchant inherits | T1 |
| PayPal | Global backbone — PayPal-branded checkout | T1 |
| Telr | MENA / GCC corridor | T2 |
| Bonum | Mongolia / GCC corridor | T2 |
| PayMob | Egypt / MENA corridor | T2 |
You don't see this complexity at integration time. Your checkout calls the
[checkout-intent-resolver](/api-reference/public/checkout-intent-resolver) and
the resolver picks the optimal authorized PSP per transaction based on your
buyer's location, currency, and your authorized-PSP pool. Customer-facing
buttons surface payment-method categories (Credit Card / PayPal / BNPL / Crypto)
— never PSP brand names — except where the brand IS the method (PayPal,
Apple Pay).
## Discoverability — MCP + ACP
Agent shopping isn't a future surface. It's already live:
| Surface | Endpoint | What agents do here |
| ------------------------ | ---------------------------------------------------------- | -------------------------------------------------------------- |
| Per-merchant MCP | `mcp.droplinked.com/{your-shop-slug}/...` | Inventory queries, cart construction, checkout intent creation |
| ACP feed | `apiv3.droplinked.com/feed/acp.json` | Catalog-wide discovery for shopping agents |
| Storefront advertisement | `` in your custom storefront `` | Auto-discovery for agents browsing your domain |
See [Storefront MCP Discovery](/agentic/storefront-mcp-discovery) for the
advertisement-tag pattern and
[Connect Your Store](/agentic/connect-your-store) for the per-shop onboarding
flow.
## Upgrade preview
Tier promotion is mechanical. As your Schema C repayment-history attestations
accumulate, the rollup at
[`GET /v2/upgrade-preview`](/api-reference/public/upgrade-preview) returns:
* Your **current credit tier** (T1 / T2 / T3 / T4)
* **Points-to-next-tier** based on cumulative repayment volume + on-time rate
* **What unlocks** at the next tier (max credit-line uplift, additional lender
eligibility, lower-friction routing)
The trust-fabric reconciler runs every 6 hours, mirroring lender +
service-provider current status onto active attestations via the
`lenderCurrentStatus` / `attestorCurrentStatus` fields. **The reconciler never
auto-revokes** — see
[Trust Fabric — Issuer-state mirroring](/concepts/trust-fabric#issuer-state-mirroring).
## Audit visibility — for you
You can request your full activity log via the operator. The log captures:
* LenderRegistry / MethodologyRegistry status changes that affect your active
attestations
* Every Schema B credit-risk attestation mint targeting your `merchantId`
* Every Schema C repayment-history event linked to your settlements
* Tier promotion + projection deltas surfaced by
[`GET /v2/upgrade-preview`](/api-reference/public/upgrade-preview)
The log is append-only at the operator console (no delete path). You can hand a
slice of it to a regulator, a new lender, or a new fulfillment partner without
needing droplinked's continued cooperation.
## Privacy commitment
**PII never leaves the operator console.** Legal entity name, contact details,
banking info, KYB documents — none of it surfaces on `/v2/lenders/*`,
`/v2/methodologies/*`, or the public credit-risk attestation endpoint. Public
verifier reads return only aggregate identifiers + chain-anchored claims.
The recently-shipped `merchant-attestation-policy` enforces privacy-aware reads
inside agent-callable inventory tools too — agents querying your shop see
shoppable claims, never operator-only metadata. This is enforced at the policy
layer, not just by convention.
## What you DON'T have to do
* **Integrate 5 PSPs individually** — one droplinked integration covers Stripe,
PayPal, Telr, Bonum, PayMob. The resolver picks per transaction.
* **Build a custom credit-application UI** — `/v2/lender-routing/recommend`
returns ranked lenders for your jurisdiction. Use droplinked's routing.
* **Build agent-shoppable plumbing yourself** — the per-merchant MCP server +
ACP feed are platform-shared infrastructure.
* **Run your own brand attestation infrastructure** — Schema A
(`BrandAttestation`) is platform-shared. Your shop slug inherits the
verifiability without you minting anything.
* **Negotiate jurisdiction-by-jurisdiction with lenders** — the
[LenderRegistry](/api-reference/public/lender-registry) + routing surface
abstract that work away.
## Comparison vs. traditional commerce platforms
| Capability | Shopify + Stripe | Droplinked |
| ------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
| Credit access | Stripe Capital (US/UK/CA only, opaque scoring) | Verifiable Schema B from any registered lender, methodology-pinned, jurisdiction-routed |
| Multi-PSP routing | Single PSP per shop; multi-PSP requires custom build | 5 PSPs from one integration, resolver picks per transaction |
| Agent shoppability | Custom MCP / API work per shop | Per-merchant MCP + ACP feed live by default |
| Audit verifiability | Vendor-side logs only; regulator must trust the vendor | On-chain attestations + append-only registry audit log; regulator verifies without trusting droplinked |
| Onboarding speed | Weeks per PSP, weeks per credit product | Single platform onboarding; PSPs + credit unlock as you go |
## Related
* [Brand attestation: request → mint → verify](/guides/trust-fabric/brand-attestation-lifecycle) — end-to-end Schema A lifecycle walkthrough
* [Trust Fabric (EAS Schema v2)](/concepts/trust-fabric) — 4-axis architecture overview
* [Forensic Chain Workflow](/concepts/forensic-chain) — end-to-end verifier walkthrough
* [DeFi Lender Onboarding](/concepts/for-defi-lenders) — the lender side of this story
* [Upgrade Preview](/api-reference/public/upgrade-preview) — `/v2/upgrade-preview` reference
* [Checkout Intent Resolver](/api-reference/public/checkout-intent-resolver) — multi-PSP routing reference
* [Lender Routing](/api-reference/public/lender-routing) — `/v2/lender-routing/recommend` reference
* [Connect Your Store](/agentic/connect-your-store) — per-shop agent-onboarding flow
* [Storefront MCP Discovery](/agentic/storefront-mcp-discovery) — `` advertisement pattern
# Service Providers on Droplinked
Source: https://docs.droplinked.com/concepts/for-service-providers
Plug a 3PL / WMS / fulfillment partner into droplinked's commerce-trust fabric. Issue Schema D peer-trust attestations, route to verified merchants by archetype + track record, and build chain-anchored reputation.
Droplinked sits between **on-chain trust** and **real-world fulfillment**. If you run
a 3PL, a WMS, or a broader fulfillment partner — Stord, Flowspace, ShipBob, or a
regional operator — this page is the canonical onboarding narrative for plugging
into droplinked as a merchant-discovery channel and a Schema D peer-trust issuer.
The value exchange is three-sided. Verified merchants onboarding on droplinked need
a fulfillment partner. The operator routes them to ACTIVE service providers in your
archetype, ranked by track record. Every successful ingestion increments your
`successfulIngestionCount` — your rank compounds with usage rather than decaying
with time. Meanwhile, your registered entity becomes an authorized Schema D issuer:
you can attest other entities in the trust fabric (other SPs, lenders, the
merchants you've directly worked with) and surface those peer-trust signals into the
multi-axis dossier consumer agents pull.
The yield is **operator-curated routing to qualified merchants + chain-anchored
reputation that compounds + an attestation-issuance surface no industry directory
offers**. Not a directory listing that decays. Not paid placement. A track-record
ranking that improves every time you successfully onboard a merchant routed your way.
## What you gain
Verified merchants discover you via `/v2/service-provider-routing/recommend` — a public
routing surface that ranks ACTIVE SPs by archetype + track record. No paid placement.
Your registered signing wallet becomes an authorized issuer of Schema D
`CrossAttestation`s — peer-trust scores on other entities in the trust fabric.
`successfulIngestionCount` increments on every successful merchant ingestion.
The more merchants you onboard, the higher you rank in the next routing pass.
Every lifecycle event lands in `service-provider-audit-log` — append-only,
operator-curated, regulator-readable. Your track record is not a marketing claim.
Status changes, metadata edits, ingestion-count bumps — all preserved forever.
Regulators querying the operator console reconstruct any state at any past timestamp.
Consumer agents (ChatGPT, Claude, Cursor) call `recommend_service_provider` and
surface you directly to onboarding merchants — no partner BD required.
## Architecture at a glance
```mermaid theme={null}
flowchart TD
A["3PL / WMS / fulfillment partner (your signing wallet)"] --> B["ServiceProviderRegistry (operator-curated, SUPER_ADMIN-gated)"]
B --> C["Schema D CrossAttestation (peer-trust scores)"]
B --> D["/v2/service-provider-routing/recommend (public routing surface)"]
E["Merchant onboarding"] --> D
D --> F["Track-record-based ranking (successfulIngestionCount desc, lastSuccessfulIngestionAt DESC tiebreak)"]
F --> G["Merchant routed to your endpoint"]
G --> H["Successful ingestion increments successfulIngestionCount"]
H --> F
```
Every node above is either a registry mutation captured in an append-only audit
log, an on-chain attestation, or a public read endpoint a verifier can hit without
trusting droplinked. A regulator querying via the operator console can reconstruct
the full lifecycle of any service provider in scope — every status flip, every
metadata edit, every counter increment — at any past timestamp.
## Why droplinked vs. direct sales?
Traditional 3PL discovery is a slow, fragmented BD slog. Each merchant prospect
requires its own outreach, pricing conversation, security-questionnaire pass, and
slow trust-building. Industry directories list you statically with no track-record
signal. Operator-curated discovery on droplinked is structurally different:
* **Direct outreach**: per-merchant BD effort, fragmented integration, slow
trust-building. Reputation lives in salesperson lore, not in a verifiable record.
* **Industry directories**: static listings, no track-record signal, paid placement
distorts rank, no chain-anchored proof of successful onboardings.
* **Droplinked**: one operator-curated integration surfaces you to many qualified
merchants. Your chain-anchored reputation accrues with every successful
ingestion. Consumer agents recommend you autonomously via MCP.
The discovery surface is one integration. The reputation surface is on-chain and
append-only. Both compound.
## The 3-step onboarding
The operator console gates SP registration (SUPER\_ADMIN). Provide the
legal entity name, archetype (`stord` / `flowspace` / `shipbob` / `generic`),
jurisdiction, and signing wallet that will sign Schema D attestations.
```bash theme={null}
curl -X POST https://apiv3.droplinked.com/admin/service-providers \
-H "Authorization: Bearer $SUPER_ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{
"providerId": "your-sp-slug",
"displayName": "Your SP (Region)",
"archetype": "stord",
"jurisdiction": "US",
"signingWallet": "0x0000000000000000000000000000000000000000"
}'
```
**What changes**: a new row in `ServiceProviderRegistry` (status `PENDING_KYB`)
* a `SERVICE_PROVIDER_REGISTERED` event in `service-provider-audit-log`
(preserved forever).
Once your KYB is verified by droplinked, the operator flips your status
to `ACTIVE`. Only ACTIVE SPs appear in the public routing surface and
only ACTIVE SPs can mint Schema D attestations.
```bash theme={null}
curl -X POST https://apiv3.droplinked.com/admin/service-providers/your-sp-slug/status \
-H "Authorization: Bearer $SUPER_ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{ "newStatus": "ACTIVE", "reason": "KYB verified" }'
```
**What changes**: `ServiceProviderRegistry.status` flips to `ACTIVE` + a
`SERVICE_PROVIDER_STATUS_CHANGED` event lands in
`service-provider-audit-log`. The `EasIssuer.isActive` gate now lets your
wallet sign Schema D attestations, and you become discoverable via
`/v2/service-provider-routing/recommend`.
Merchants routed to you via the recommendation surface integrate with your
ingestion endpoint. On each successful onboarding, the operator increments
`successfulIngestionCount` + stamps `lastSuccessfulIngestionAt`. Your rank
in the next routing pass rises.
No request from you — the counter is operator-controlled signal, not
self-reportable. The append-only audit log captures every increment.
## What verifiers see
Once registered + ACTIVE, your offering surfaces via these endpoints:
| Endpoint | Visibility | Surfaces |
| -------------------------------------------------------------------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `GET /admin/service-providers/:providerId` | SUPER\_ADMIN-gated | Full admin profile (signing wallet, contact, ingestion counter, status, archetype, jurisdiction) |
| `GET /admin/service-providers/:providerId/timeline` | SUPER\_ADMIN-gated | Full unredacted lifecycle (registered → status changes → metadata edits → ingestion increments) |
| [`GET /v2/service-provider-routing/recommend?archetype=stord`](/api-reference/public/service-provider-routing) | Public, read-only | Your discoverable presence — ranked by `successfulIngestionCount` desc, ties broken by most-recent successful ingestion |
**Why no public `GET /v2/service-providers/:id` endpoint?** Partner SPs interact
with you via the **routing layer**, not a direct lookup. Merchants don't browse
SPs by id; they receive an ordered list filtered by archetype. The admin profile
endpoints are SUPER\_ADMIN-gated because the underlying record carries operator
notes + contact metadata that isn't intended for public surface. Schema D
attestations you issue are the public-readable artifact tying you to other
entities — that's the verifier-readable trust signal.
## Schema D peer-trust attestation
Schema D is the trust fabric's **peer-trust axis**. Any registered + ACTIVE entity
in the trust fabric — service providers, lenders, eventually merchants — can attest
any other entity with a 0–100 trust score + a free-form basis explaining why.
```typescript theme={null}
// Conceptual Schema D mint via SDK (operator-coordinated for now)
{
subject: 'flowspace-us-east', // the entity you're attesting
subjectType: 'service-provider',
issuer: 'stord-us-east-1', // you
trustScore: 87, // 0-100
basis: 'Co-handled 12 Q2 fulfillment overflow batches without exception.',
expiresAt: '2027-06-13T00:00:00Z'
}
```
The mint lands as a `CrossAttestation` on the [Ethereum Attestation
Service](https://attest.org) (currently Base Sepolia testnet; mainnet pending KMS
migration). The attestation is append-only — supersession is via a new mint
referencing the prior `attestationUid`, never via in-place edit. Every divergence
between a marketing claim and an on-chain mint is provably detectable.
Schema D outputs feed:
* [`GET /v2/attestations/cross/subject/:rootUid`](/concepts/trust-fabric) — all peer-trust
attestations *about* a given entity
* [`GET /v2/attestations/cross/issuer/:rootUid`](/concepts/trust-fabric) — all peer-trust
attestations *issued by* you
* The MCP `get_trust_dossier` tool — composite multi-axis trust dossier consumer
agents pull when underwriting or recommending counter-parties
See [Trust Fabric — the 4 axes](/concepts/trust-fabric#the-4-axes) for how Schema D
sits alongside Schemas A (brand), B (credit-risk), and C (repayment-history).
## Routing rank — how track record turns into discovery
The routing surface ranks ACTIVE SPs in two passes:
1. **Archetype match** — if the query includes `?archetype=stord`, only SPs with
`archetype === 'stord'` are eligible. Without a filter, all archetypes compete.
2. **Track-record sort** within the eligible set:
* **Primary**: `successfulIngestionCount` desc — proven track record first.
* **Tiebreak**: `lastSuccessfulIngestionAt` desc — most-recent success wins ties.
Newcomers compete on archetype-match first (you'll always rank against your peers
in your archetype, not against unrelated SPs). Then track record. A new SP with
zero ingestions ranks below an established SP in the same archetype until the first
successful ingestion lands; from there, every successful ingestion bumps your
ranking.
The counter is **operator-controlled signal** — increments fire from the operator
console on confirmed successful onboardings, not from a self-reportable callback.
That's deliberate. Self-reported metrics decay into noise; operator-curated metrics
keep the routing surface useful to merchants.
## Admin audit visibility
**Every lifecycle event on your row is preserved in `service-provider-audit-log`**.
Status flips (`PENDING_KYB` → `ACTIVE` → `SUSPENDED` → `ARCHIVED`), metadata edits
(display name, jurisdiction, contact notes, signing wallet rotation),
ingestion-count increments — all append-only, all reconstructable. The operator
console can replay any past state at any past timestamp. Regulators querying via
the operator have full visibility. There is no delete path.
## MCP agent surface
Consumer agents — ChatGPT, Claude, Cursor, OpenAI Agents SDK — call the
`recommend_service_provider` tool on the [droplinked-mcp
server](/agentic/mcp-server) and surface you directly to onboarding merchants
without partner BD:
```typescript theme={null}
const recs = await mcp.callTool('recommend_service_provider', {
archetype: 'stord',
limit: 5,
});
```
```json theme={null}
{
"archetype": "stord",
"count": 1,
"recommendations": [
{
"providerId": "stord-us-east-1",
"displayName": "Stor'd US-East",
"successfulIngestionCount": 47,
"lastSuccessfulIngestionAt": "2026-06-11T14:30:00Z",
"rank": 1
}
]
}
```
Pair `recommend_service_provider` with [`verify_brand_attestation` +
`verify_credit_risk`](/concepts/trust-fabric#mcp-tool-surface) for the full
forensic chain — confirm a merchant's brand + credit-risk posture before
investing onboarding cycles in them.
The MCP tool catalog is currently published under
[/agentic/lender-trinity-mcp-tools](/agentic/lender-trinity-mcp-tools) for
historical reasons — the page covers both lender + service-provider recommendation
tools (`recommend_lender` + `recommend_service_provider`) on the same MCP server.
## Operator gates
**Actual onboarding is operator-gated (SUPER\_ADMIN).** The endpoints in the 3-step
flow above are admin-only — droplinked's operator console executes them on your
behalf after verifying your KYB packet.
To start, contact `support@droplinked.com` with:
* Legal entity name + corporate jurisdiction
* Regulator / accreditation references (any IATA, ISO 9001, SOC 2, GDP / GxP, FDA
registrations relevant to your archetype)
* Signing wallet address (EVM, 20-byte) — this is the wallet that will sign
Schema D `CrossAttestation`s
* Archetype: `stord` / `flowspace` / `shipbob` / `generic`
* Jurisdiction: ISO 3166-1 alpha-2 (e.g. `US`, `AE`, `SG`) or `GLOBAL` if you serve
unrestricted geography
* Ingestion endpoint integration spec — your existing webhook / API contract so
the operator can wire merchants routed to you straight into your pipeline
Mainnet for Schema D mints is currently gated on the KMS-backed signer migration.
Routing-surface discovery + ingestion-counter accrual are live today; on-chain
attestation issuance proceeds against Base Sepolia testnet until the mainnet flip.
## Comparison vs traditional 3PL discovery channels
| Channel | Discovery model | Track-record signal | Reputation surface | Cost |
| ------------------------ | -------------------------------- | ------------------------------------------------ | ----------------------------------- | ------------------------------------- |
| **Direct outreach** | Per-merchant BD | None (lives in CRM notes) | Sales testimonials | High BD overhead, slow |
| **Industry directories** | Static listing | None (or self-reported) | Paid placement distorts | Listing fee, low intent |
| **Droplinked** | Operator-curated routing surface | `successfulIngestionCount` (operator-controlled) | Chain-anchored Schema D + audit log | One integration, compounds with usage |
The asymmetry is structural. Direct outreach scales linearly with BD headcount.
Directory listings are a one-time fee for a static surface. Droplinked routing
compounds: every successful onboarding raises your rank for the next merchant in
your archetype, and every Schema D attestation you issue makes you a more weighty
node in the trust fabric.
## Related
* [Trust Fabric (EAS Schema v2)](/concepts/trust-fabric) — 4-axis architecture overview
* [DeFi Lender Onboarding](/concepts/for-defi-lenders) — sibling onboarding guide for the credit-risk axis
* [Merchants on Droplinked](/concepts/for-merchants) — sibling guide for the merchant side of the marketplace
* [Service-Provider Routing](/api-reference/public/service-provider-routing) — `/v2/service-provider-routing/recommend` reference
* [Lender Trinity MCP Tools](/agentic/lender-trinity-mcp-tools) — covers `recommend_service_provider` + sibling tools
* [Forensic Chain Workflow](/concepts/forensic-chain) — end-to-end verifier walkthrough
# Forensic Chain Workflow
Source: https://docs.droplinked.com/concepts/forensic-chain
End-to-end walkthrough — how a verifier audits a Schema B credit-risk attestation from the on-chain UID back to the lender, the methodology, and the merchant's repayment history.
A consumer agent or external verifier presented with a Schema B credit-risk attestation can independently audit every claim it makes by composing the [trust fabric MCP tools](/concepts/trust-fabric). This page walks through the canonical forensic chain.
## Scenario
A consumer agent is about to extend BNPL terms based on a merchant's credit-risk attestation. The agent wants to verify, end-to-end:
1. The attestation exists on-chain at the cited UID
2. The lender that issued it is real, ACTIVE, and licensed
3. The methodology the lender cited is the one they actually published
4. The merchant's repayment history matches the tier on the attestation
## Step 1: read the attestation
```typescript theme={null}
const attestation = await mcp.callTool('verify_credit_risk', {
merchantId: '6a207db0d29923bffaa983ca',
lenderId: 'crediblex-uae',
});
```
```json theme={null}
{
"found": true,
"attestationUid": "0xattest...",
"lenderId": "crediblex-uae",
"creditTier": "T2",
"maxCreditLineUsdCents": 5000000,
"issuedAt": "2026-06-01T12:00:00Z",
"expiresAt": "2026-09-01T12:00:00Z",
"status": "ACTIVE",
"lenderCurrentStatus": "ACTIVE",
"issuerWallet": "0xD5F6...",
"methodologyHash": "0xmeth..."
}
```
The agent reads:
* `attestationUid` — the on-chain UID
* `lenderId` — who issued it
* `issuerWallet` — the wallet that signed
* `methodologyHash` — the underwriting basis cited
* `lenderCurrentStatus` — the **registry-state mirror** (updated every 6h by the reconciler)
**Watch the mirror**: `status: ACTIVE` means the attestation is on-chain valid. `lenderCurrentStatus: SUSPENDED` would mean the lender has been de-listed in the registry since mint. Verifier-side policy decides whether to honor an attestation from a since-de-listed lender.
## Step 2: verify the lender
```typescript theme={null}
const lender = await mcp.callTool('verify_lender', {
lenderId: attestation.lenderId,
});
```
```json theme={null}
{
"found": true,
"lenderId": "crediblex-uae",
"displayName": "CredibleX (UAE)",
"archetype": "fsra-licensed",
"jurisdiction": "AE",
"status": "ACTIVE",
"signingWallet": "0xD5F6...",
"regulatorReference": "FSRA-12345",
"issuedAttestationCount": 42,
"lastAttestationAt": "2026-06-11T22:00:00Z"
}
```
**Cross-check**: `lender.signingWallet` must equal `attestation.issuerWallet`. A mismatch indicates schema impersonation — someone signed an attestation pretending to be CredibleX. The agent **must refuse** in that case.
## Step 3: verify the methodology
```typescript theme={null}
const methodology = await mcp.callTool('verify_methodology', {
lenderId: attestation.lenderId,
methodologyHash: attestation.methodologyHash,
});
```
```json theme={null}
{
"found": true,
"lenderId": "crediblex-uae",
"version": "2026.06.1",
"methodologyHash": "0xmeth...",
"documentUrl": "https://crediblex.example.com/methodology-2026-06-1.pdf",
"displayName": "CredibleX Inventory-Financing v2",
"status": "ACTIVE",
"effectiveAt": "2026-06-01T00:00:00Z",
"supersededAt": null
}
```
**Off-tool cross-check**:
1. Download `methodology.documentUrl`
2. Compute SHA-256 of the document bytes
3. Compare against `methodology.methodologyHash`
Any divergence flags methodology tampering. The verifier **must refuse** if the hashes don't match.
If `methodology.status` is `SUPERSEDED`, the attestation cites a methodology version the lender has since updated. That's acceptable — old attestations stay tied to their original methodology — but the verifier should note it.
If `methodology.status` is `REVOKED`, the operator has pulled this methodology. The attestation should be considered invalid pending re-issuance.
## Step 4: verify the repayment-history rollup
```typescript theme={null}
const signals = await mcp.callTool('get_underwriting_signals', {
merchantId: attestation.merchantId,
});
```
```json theme={null}
{
"creditRisk": {
"maxObservedTier": "T3",
"latestPerLender": [
{ "lenderId": "crediblex-uae", "creditTier": "T2",
"lenderCurrentStatus": "ACTIVE", "status": "ACTIVE" }
]
},
"repaymentHistory": {
"merchantWide": {
"totalSettlements": 17, "totalOnTime": 13,
"totalLate": 3, "totalDefaults": 1
}
},
"summary": {
"anchorTier": "T3",
"reliabilityScore": 76
}
}
```
**Cross-check**:
* `signals.summary.anchorTier` = `max(observed-from-repayment, already-issued)` — load-bearing risk signal
* `signals.summary.reliabilityScore` — overall on-time fraction
* If the verifier needs to know whether the attestation's `creditTier` is *currently* justified by the repayment history, compare with `signals.upgradeEligibility.observedTier`
## Step 5: independent on-chain verification
The agent can quote `attestationUid` + `chain` back to the end-user with a link to:
```
https://base-sepolia.easscan.org/attestation/view/0xattest...
```
…where the user can see the on-chain receipt without trusting droplinked's API.
## Decision tree
| Check | If pass | If fail |
| --------------------------------------------------------- | ------- | ------------------------------------------------------ |
| Attestation found + `status: ACTIVE` | Proceed | Refuse |
| `lenderCurrentStatus: ACTIVE` | Proceed | Verifier policy decides |
| Lender found + `signingWallet` matches `issuerWallet` | Proceed | **Refuse — schema impersonation** |
| Methodology found + hash matches downloaded doc | Proceed | **Refuse — methodology tampering** |
| Repayment-history rollup consistent with attestation tier | Proceed | Verifier policy decides (could be a stale attestation) |
| On-chain UID resolves on easscan.org | Proceed | **Refuse — UID fabricated** |
## End-to-end verifier walkthrough
The MCP tool chain above is the agent-runtime shortcut. A third-party verifier — a
regulator, a counter-party PSP, an external auditor — can walk the exact same proof using
the **public HTTP read endpoints** with no MCP client at all. Five steps, all public, no
JWT, no IP-allowlist.
### 1. Read the Schema B attestation
```bash theme={null}
curl -s https://apiv3.droplinked.com/v2/attestations/credit-risk/$MERCHANT_ID
```
Returns `attestationUid`, `lenderId`, `methodologyHash`, `issuerWallet`, `occurredAt`,
`creditTier`, `status`, and the registry-mirror fields (`lenderCurrentStatus` etc.). The
verifier pins `occurredAt` — every downstream check is evaluated **as-of that timestamp**,
not as-of now.
### 2. Resolve the issuing lender
```bash theme={null}
curl -s https://apiv3.droplinked.com/v2/lenders/$LENDER_ID
```
See [Lender Registry Lookup](/api-reference/public/lender-registry) for the full response
shape. The verifier cross-checks `lender.signingWallet === attestation.issuerWallet`; a
mismatch is schema impersonation and the attestation must be refused.
### 3. Resolve the methodology
```bash theme={null}
curl -s https://apiv3.droplinked.com/v2/methodologies/$LENDER_ID/$METHODOLOGY_HASH
```
See [Methodology Registry Lookup](/api-reference/public/methodology-registry) for the
response shape. Returns the `documentUrl` + `status` + lifecycle timestamps
(`effectiveAt`, `supersededAt`).
### 4. Download + re-hash the methodology document
The verifier downloads `documentUrl`, computes SHA-256 of the bytes, and compares against
the on-chain `methodologyHash`. The exact recipe (with a working `curl` + `shasum`
example) lives on the [Methodology Registry Lookup](/api-reference/public/methodology-registry#verifier-integrity-check)
page — don't re-implement it, link to it.
A divergence here is methodology tampering and the attestation must be refused regardless
of every other check passing.
### 5. Inspect the public lifecycle timeline
```bash theme={null}
curl -s https://apiv3.droplinked.com/v2/methodologies/$LENDER_ID/$METHODOLOGY_HASH/timeline
```
The methodology lifecycle timeline endpoint surfaces the full
`DRAFT → ACTIVE → SUPERSEDED → REVOKED` transition history for a methodology, with
timestamps + the operator/event that drove each transition. A verifier composes the
transition history against the attestation's `occurredAt` to make a **time-honest** policy
call:
* A methodology that's currently `SUPERSEDED` but was `ACTIVE` at attestation `occurredAt`
is **still legitimate** — the lender used the methodology that was in force at the time,
and the attestation should be honored.
* A methodology that's `REVOKED` is a **red flag** regardless of when the attestation was
issued. The operator has retroactively pulled this methodology; verifier-side policy
decides whether to honor (most verifiers refuse).
The timeline endpoint replaces ad-hoc "look at the audit log" requests with a public,
verifier-callable surface. No SUPER\_ADMIN access required.
### Cross-links
* [Trust Fabric overview](/concepts/trust-fabric) — where each registry sits in the 4-axis fabric
* [Methodology Registry Lookup](/api-reference/public/methodology-registry) — request/response shape + integrity-check recipe
* [Lender Registry Lookup](/api-reference/public/lender-registry) — `signingWallet` cross-check field reference
## Related
* [Trust Fabric overview](/concepts/trust-fabric)
* [Underwriting MCP Tools](/agentic/underwriting-mcp-tools)
* [Lender Trinity MCP Tools](/agentic/lender-trinity-mcp-tools)
# InventoryOS
Source: https://docs.droplinked.com/concepts/inventory-os
Droplinked's catalog + inventory bridge layer — accepts Shopify, WooCommerce, BigCommerce, Magento, or custom-storefront catalogs and unlocks agentic-commerce distribution, on-chain trust-fabric attestation, and licensed-lender credit-line eligibility without re-platforming.
InventoryOS is droplinked's **catalog + inventory bridge layer**. It mirrors your existing commerce platform's catalog into a normalized droplinked-internal representation that the rest of the droplinked stack — agentic distribution, the trust fabric, the LenderRegistry — reads against. You don't move products, customers, or checkouts; you mirror the catalog and order events that drive distribution and underwriting decisions.
The mirror is **one-way** (your platform stays the source of truth) but **reflective**: stock-level updates, product create/update/delete events, and order events flow into droplinked so the agentic and financing surfaces stay current with what your storefront actually shows. Conflict resolution always falls back to the origin platform for price, title, description, and stock — droplinked annotates the mirror with attestation pointers and agent-shoppable metadata.
This bridge unlocks four downstream capabilities — **MCP agent surfaces**, **ACP feed inclusion**, **brand attestation** (Schema A), and **lender-routing eligibility** (Schema B) — without your team re-platforming. The connector pattern generalizes across Shopify (live today), WooCommerce, BigCommerce, Magento, and any custom storefront that can emit webhooks.
## The normalized layer model
InventoryOS sits between origin commerce platforms and the droplinked-side surfaces that consume normalized catalog + order data.
```mermaid theme={null}
flowchart TD
A1[Shopify] --> IOS
A2[WooCommerce] --> IOS
A3[BigCommerce] --> IOS
A4[Magento] --> IOS
A5[Custom storefront] --> IOS
IOS[InventoryOS normalized catalog + order events]
IOS --> B1[Per-merchant MCP agentic distribution]
IOS --> B2[ACP feed agent-shoppable index]
IOS --> B3[Lender routing financing eligibility]
IOS --> B4[Brand attestation on-chain trust]
```
The same normalized representation feeds every droplinked-side reader. Adding a new platform adapter means writing one webhook receiver — every downstream surface inherits the new source automatically.
## What InventoryOS stores per merchant
For each connected merchant, InventoryOS persists a thin, distribution-oriented slice of catalog + order state:
* **Normalized product catalog** — titles, descriptions, variants, images, vendor, tags, SKU
* **Stock levels** — reflective; the origin platform stays the source of truth, droplinked never authoritatively claims a unit you didn't tell us about
* **Recent order events** — for [Schema C repayment-history](/concepts/trust-fabric) attribution + affiliate tracking + agent-source revenue attribution
* **Connection metadata** — origin platform domain, webhook secret hash, last-sync timestamp, signing-validation mode
* **Trust-fabric pointers** — Schema A attestation UID, current lender associations (Schema B), current methodology hash, repayment-history rollup
The normalized product record looks approximately like this — the same shape regardless of whether it came from Shopify, WooCommerce, BigCommerce, Magento, or a custom adapter:
```json theme={null}
{
"droplinkedMerchantId": "mch_…",
"originPlatform": "shopify",
"originId": "gid://shopify/Product/9876543210",
"shopSlug": "your-shop-slug",
"title": "Carbon Hoodie",
"description": "…",
"vendor": "Your Brand",
"tags": ["hoodie", "carbon"],
"variants": [
{ "originVariantId": "…", "sku": "CARBHOOD-M", "size": "M", "price": { "currency": "USD", "amount": 4900 }, "stock": 42 }
],
"images": ["https://cdn.example.com/carbhood.jpg"],
"trustFabric": {
"brandAttestationUid": "0xattest…",
"currentLenderIds": ["crediblex-uae"],
"currentMethodologyHash": "0xmeth…"
},
"lastSyncedAt": "2026-06-13T12:00:00Z"
}
```
## What InventoryOS does NOT store
By design, InventoryOS stays out of surfaces that are already owned by the origin platform or by another droplinked subsystem:
* **Customer PII** — names, addresses, account history all stay on the origin platform
* **Payment credentials** — card data, PSP secrets, and tokenization stay with the PSP backbone
* **Origin-platform admin data** — reports, analytics, app installs, theme content
* **Custom-app data** — apps installed on Shopify (or any other origin) keep their data on the origin
The smaller the InventoryOS surface area, the simpler the privacy + revocation posture downstream.
## The connector pattern
Every platform adapter follows the same shape:
* **Webhook receivers** for product `create` / `update` / `delete`, inventory-level updates, and order events
* **Bulk sync** for backfill — pages through the origin platform's catalog API (when OAuth is available) to mirror products that pre-date the webhook subscription
* **HMAC (or platform-equivalent) validation** on every inbound webhook — Shopify and WooCommerce use SHA256 HMAC, BigCommerce uses signed JWT, Magento uses RSA signing
* **Idempotent upserts** keyed on the origin-platform product ID — replays and retries don't double-write
* **Conflict resolution** — origin platform wins on price / title / description / stock; InventoryOS annotates the mirror with attestation pointers and agent-shoppable metadata
Each connector ships as its own ingestion endpoint + signing-validation module:
* [Connect Shopify](/agentic/connect-shopify) — live today; the most fully-built reference
* [Connect WooCommerce, BigCommerce, Magento, or custom](/agentic/connect-other-platforms) — same pattern, per-platform webhook setup
* [Connect your store (generic shape)](/agentic/connect-your-store) — platform-agnostic surface
## Order events + attribution
Order webhooks do more than reconcile stock — they're how droplinked attributes agent-sourced revenue and how Schema C repayment-history accrues.
When an order webhook lands at InventoryOS, the event records the order ID, the items + variants, and a **buyer-source tag** that controls attribution:
* **From MCP / agent** — revenue attributes back to the discovering agent + any upstream affiliate, settled via x402 on the 70/20/10 split
* **From storefront direct** — standard order; no agent attribution, no affiliate cut
* **From ACP feed buyer surface** — ACP attribution path, settles via Stripe ACP
The buyer-source tag is set at order-create time by the surface that originated the conversion — the per-merchant MCP stamps `mcp-agent` on `start_checkout` invocations, the ACP buyer surface stamps `acp`, and any other path defaults to `direct`. The order webhook from the origin platform carries this tag forward in metadata (Shopify `note_attributes`, WooCommerce `meta_data`, BigCommerce custom fields, etc.) so InventoryOS doesn't need to guess the source after the fact.
A normalized order event in InventoryOS looks roughly like this:
```json theme={null}
{
"droplinkedMerchantId": "mch_…",
"originPlatform": "shopify",
"originOrderId": "gid://shopify/Order/123456789",
"shopSlug": "your-shop-slug",
"occurredAt": "2026-06-13T12:00:00Z",
"buyerSource": "mcp-agent",
"agentId": "agent_chatgpt_…",
"affiliateId": "aff_…",
"items": [
{ "originVariantId": "…", "sku": "CARBHOOD-M", "quantity": 1, "price": { "currency": "USD", "amount": 4900 } }
],
"totals": { "currency": "USD", "subtotal": 4900, "tax": 392, "shipping": 800, "grand": 6092 },
"settlement": { "psp": "stripe", "settlementId": "py_…", "lenderId": null }
}
```
The same order event also feeds the **Schema C repayment-history attestation** when the merchant has an active credit line: the settlement ID, amount, and lender are recorded against the merchant's append-only repayment record, which drives [tier upgrades](/api-reference/public/upgrade-preview) over time. Refunds and chargebacks emit their own append entries — repayment history is full event-log, not just net.
## Reading from InventoryOS
Downstream droplinked-side surfaces never query the origin platform directly — they read from the InventoryOS mirror. This keeps the read path uniform regardless of origin and shields the origin's rate budget from agent traffic.
* **Per-merchant MCP tools** — `search_products`, `get_product`, `list_shop_products`, `start_checkout` all read from the merchant's InventoryOS slice scoped to their `shopSlug`
* **ACP feed builder** — the nightly + on-update build of `apiv3.droplinked.com/feed/acp.json` pages through every connected merchant's InventoryOS catalog and emits a normalized ACP item per variant
* **Lender-routing recommendation** — `GET /v2/lender-routing/recommend` reads the merchant's InventoryOS activity (revenue volume, channel mix, repayment streak) to weight lender ordering
* **Underwriting signals** — `GET /v2/underwriting-signals/:merchantId` composes Schema B + Schema C reads from the trust-fabric pointers stored on the InventoryOS record
* **Affiliate attribution** — order events carry the `buyer-source` tag forward, and the affiliate-network worker reads order events from InventoryOS to settle x402 payouts
Same normalized record, every reader.
## Trust-fabric tie-in
InventoryOS is the activity feed that the [trust fabric](/concepts/trust-fabric) reads against to decide what's claimable:
* **Schema A — brand attestation.** Operator-curated KYB. A brand attestation becomes claimable once InventoryOS has roughly 30 days of consistent catalog activity from the same merchant (so the brand-slug binding has a meaningful track record).
* **Schema B — credit-risk attestation.** Methodology-hashed at mint time. When a lender underwrites a merchant, the methodology hash from the published [MethodologyRegistry](/api-reference/public/methodology-registry) entry is pinned into the on-chain attestation alongside the credit-tier.
* **Schema C — repayment-history attestation.** Accrues append-only from order events flowing into InventoryOS. Every settled order against an active credit line adds to the merchant's rolling repayment record.
* **Tier projection.** [`GET /v2/upgrade-preview?merchantId=...`](/api-reference/public/upgrade-preview) reads InventoryOS-tracked activity (revenue volume, repayment streak, attestation age) to project what the next financing tier requires.
## Cross-channel reconciliation
The headline payoff of treating InventoryOS as a **bridge** rather than another silo: a single merchant can connect **multiple origin platforms** and get one unified droplinked-side posture.
* A merchant selling on Shopify + WooCommerce + a custom Next.js site can connect all three to InventoryOS
* **Single repayment-history** accrues across all channels — Schema C rolls up settlement events from every connected origin
* **Single lender-routing decision** based on combined activity — the lender sees the merchant's full revenue picture, not just one channel
* **Single agentic distribution surface** — one per-merchant MCP, one ACP feed entry, one brand-slug
* **Brand attestation issued once** — Schema A binds the brand-slug to the merchant wallet and applies to every connected channel
This is the operator-strategic bet behind InventoryOS: financing decisions and agentic discovery should reflect a merchant's full commercial footprint, not a single-platform slice of it.
## Operational characteristics
A few properties of the InventoryOS layer that matter when you're wiring it up:
* **Idempotent ingestion.** Every webhook is keyed on `(originPlatform, originId, eventType, occurredAt)`. Replays from the origin platform — Shopify's webhook retry policy, WooCommerce's re-fire, BigCommerce's at-least-once delivery — collapse to a single mirror state without double-counting.
* **Eventual consistency, bounded.** Per-merchant order-event lag from origin-platform write to InventoryOS visibility is typically sub-second; the cap is the origin platform's webhook delivery SLA. The reconciler cron sweeps every 6h to catch any missed events via the origin's catalog API.
* **Backfill is explicit.** Webhooks only carry events from the moment they're subscribed forward. Initial-catalog backfill runs against the origin platform's catalog API (paged products connection on Shopify, REST `/products` on WooCommerce, V3 catalog on BigCommerce, REST `/V1/products` on Magento) — kicked from the connect endpoint, async for large catalogs.
* **Per-merchant rate-limits.** InventoryOS shields the origin platform from the agent traffic on the droplinked side — agent reads hit the InventoryOS mirror, not the origin's API. Your origin-platform rate budget stays yours.
* **Audit-trail retention.** Order events and attestation pointers are append-only — even on connector revocation, the historical record is retained for verifier-side audit. See the privacy callout below for the revocation-vs-deletion split.
## Privacy + control
Customer PII stays on the origin platforms. InventoryOS sees only catalog + order metadata — the SKUs, totals, settlement IDs, and buyer-source tags needed for attribution and Schema C attestation. No customer profiles, no shipping addresses, no payment data.
Merchants can revoke any connector at any time. Revocation pauses the corresponding mirror (no new events ingested) but does not delete historical data — Schema C is an append-only audit trail, and droplinked retains the historical record for compliance and verifier-side audit, in line with the rest of the [trust fabric](/concepts/trust-fabric).
## What's coming
A realistic roadmap — what's live today is the Shopify connector + the InventoryOS internal layer; these are the next adapters and surfaces, in roughly the order we'll ship them:
* **WooCommerce connector** — Q3 2026, WordPress plugin for one-click install on top of the webhook-config dance
* **BigCommerce connector** — official BigCommerce App Marketplace listing
* **Magento 2.x connector** — Magento extension with RSA-signature verification parity
* **Custom-storefront SDK** — `@droplinked/connector-sdk` covering REST + GraphQL helpers for arbitrary platforms (Next.js, React, Express, Rails, Django)
* **Two-way sync opt-in** — per-field push of droplinked-side annotations (attestation badges, agent-revenue tags, lender-tier callouts) back into origin-platform admin notes
## FAQ
No. InventoryOS is strictly additive — a one-way mirror that reads from your origin platform via webhooks. Your storefront, checkout, customer accounts, and admin all stay on the origin platform. You can revoke any connector at any time and the origin keeps working unchanged.
They roll up into one droplinked-side merchant. One brand-slug, one Schema A attestation, one per-merchant MCP, one ACP feed entry, one lender-routing posture. Order events across all origins accrue to the same Schema C repayment-history record. See [Cross-channel reconciliation](#cross-channel-reconciliation) above.
Not today — every connector is one-way (origin → InventoryOS). Two-way sync is on the roadmap (opt-in, per-field) so droplinked-side annotations like attestation badges, agent-revenue tags, and lender-tier callouts can flow back into origin-platform admin notes. Your origin catalog and pricing stay yours.
Both are agentic-distribution surfaces, both read from InventoryOS, but they serve different agent shapes. The **per-merchant MCP** is interactive — agents call tools (`search_products`, `get_product`, `start_checkout`) scoped to one shop. The **ACP feed** is index-shaped — a single JSON document with every connected merchant's items, ingested in bulk by agentic shopping surfaces (ChatGPT shopping, Claude commerce surfaces) for discovery. A merchant inherits both automatically on connect.
InventoryOS never receives customer PII. Order events carry only the fields needed for attribution and Schema C attestation — settlement ID, totals, item lines, buyer-source tag. Customer names, addresses, and payment data stay on the origin platform.
## Related
* [Connect your Shopify store](/agentic/connect-shopify) — the live, fully-built reference adapter
* [Connect WooCommerce, BigCommerce, Magento, or custom](/agentic/connect-other-platforms) — same pattern, per-platform webhook setup
* [Connect your store](/agentic/connect-your-store) — platform-agnostic connect surface
* [Storefront MCP Discovery](/agentic/storefront-mcp-discovery) — advertise your per-merchant MCP URL from your storefront ``
* [Trust fabric](/concepts/trust-fabric) — Schema A/B/C/D attestation architecture
* [For merchants](/concepts/for-merchants) — how droplinked composes with your existing commerce stack
* [Lender Routing Recommendation](/api-reference/public/lender-routing) — pick the right lender by jurisdiction
* [Trust Fabric Statistics](/api-reference/public/trust-fabric-stats) — public rollup of verified-brand counts and credit-attestation volumes
# Platform model
Source: https://docs.droplinked.com/concepts/platform-model
The core objects and surfaces you'll work with across the Droplinked API.
Droplinked is commerce infrastructure: merchants publish inventory, customers buy it on
storefronts, and — increasingly — **AI agents discover and purchase it** on the merchant's
behalf. The same core objects power all of it.
## Core objects
A merchant's store (slug, design template, payment methods, currency). Public read via
`/shops/v2/public/name/{name}`; managed via `/shops/v2`.
Physical, digital, or print-on-demand (POD). Carries SKUs, pricing, and — for physical/POD —
a `shippingProfileId`. Public catalog via `/product-v2/public/shop/{name}`.
`/v2/carts/...` — add items, attach a customer, prepare for checkout (address verification +
shipping rates), and pay.
The result of a completed checkout (`/order-v2`), including fulfillment for POD via Printful.
## Surfaces
* **Storefront** (`droplinked.io`) — where customers browse and buy.
* **Shop Builder** (`droplinked.com`) — where merchants design the storefront and manage inventory.
* **Checkout** (`checkout.droplinked.io`) — the single-page purchase flow (contact → delivery →
shipping → payment), supporting card (Stripe), PayPal, crypto/stablecoin, and regional PSPs per
the shop's configured `paymentMethods`.
## Payments & settlement
Droplinked is **payment-rail-neutral**: merchants connect the PSP of their choice and it's
turnkey through the checkout. Card, PayPal, regional processors, and **crypto/stablecoin**
settlement are all first-class — the stablecoin path is the on-chain, low-cost differentiator.
## Agentic commerce
The same public catalog is exposed to AI agents via the **MCP server** and a **Stripe Agentic
Commerce Protocol (ACP) feed**, so a merchant's existing inventory becomes shoppable inside
ChatGPT, Claude, Cursor, and other agent surfaces. See [Agentic Commerce](/agentic/overview).
# Trust Fabric (EAS Schema v2)
Source: https://docs.droplinked.com/concepts/trust-fabric
Droplinked's 4-axis on-chain trust fabric — Schemas A/B/C/D + the LenderRegistry/ServiceProviderRegistry/MethodologyRegistry trinity that anchors them.
Droplinked publishes a **4-axis trust fabric** on the [Ethereum Attestation Service](https://attest.org) (currently on Base Sepolia testnet; mainnet pending). Every claim the platform makes about a merchant — verified brand, credit-risk underwriting, repayment performance, peer trust — is anchored to an on-chain attestation that any verifier can independently audit on [easscan.org](https://easscan.org).
## The 4 axes
| Axis | Schema | What it proves | Verifier endpoint |
| ------------------------- | ----------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------- |
| **A — Brand** | `BrandAttestation` | This brand slug controls these on-chain credentials. KYB-tier signal. | `GET /v2/attestations/brand/:slug` |
| **B — Credit-risk** | `CreditRiskAttestation` | A licensed lender has underwritten this merchant for a credit-line up to N. | `GET /v2/attestations/credit-risk/:merchantId` |
| **C — Repayment-history** | `RepaymentHistoryAttestation` | Append-only settlement record per lender. Drives tier upgrades. | `GET /v2/attestations/repayment-history/:merchantId` |
| **D — Cross** | `CrossAttestation` | Peer trust — any entity attests any other with a 0-100 score + basis. | `GET /v2/attestations/cross/{subject\|issuer}/:rootUid` |
## The registry trinity
Each axis is anchored by an operator-curated registry. A schema's on-chain attestation is only authoritative if its issuer is registered + currently `ACTIVE`.
| Registry | Anchors | Public lookup |
| ----------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| **`LenderRegistry`** | Schema B + Schema C issuers | `GET /v2/lenders/:lenderId` |
| **`ServiceProviderRegistry`** | Schema D issuers (WMS/3PL partners) | `GET /admin/service-providers/:id` (admin) |
| **`MethodologyRegistry`** | Per-lender underwriting methodology hash linked from Schema B | `GET /v2/methodologies/:lenderId/active` and `GET /v2/methodologies/:lenderId/:hash` |
## Issuer-state mirroring
The reconciler cron sweeps every 6 hours and **mirrors the registry status** onto every ACTIVE attestation:
* **Schema B** attestations carry `lenderCurrentStatus` + `lenderCurrentStatusAt`
* **Schema D** attestations carry `attestorCurrentStatus` + `attestorCurrentStatusAt`
The reconciler **never auto-revokes** an attestation. It only mirrors registry state. A verifier sees:
```json theme={null}
{
"status": "ACTIVE", // on-chain status (lifecycle)
"lenderCurrentStatus": "SUSPENDED", // registry status as of last sweep
"lenderCurrentStatusAt": "2026-06-12T01:00:00Z"
}
```
…and decides whether to honor the attestation per its own policy. **Verifier-side policy is load-bearing**: a Schema B attestation that's still on-chain ACTIVE but from a SUSPENDED lender may or may not be honored depending on context.
## Routing surfaces
For each registry, there's a **read-only recommendation endpoint** that returns the ordered list of ACTIVE issuers most-likely to serve a given merchant:
* `GET /v2/lender-routing/recommend?jurisdiction=AE` — exact-jurisdiction first, `GLOBAL` fallback
* `GET /v2/service-provider-routing/recommend?archetype=stord` — track-record sort
These endpoints **route nothing themselves** — the live application/mint paths stay gated on `isActive` at write time. Routing is a recommendation surface for portals + agents.
## Composite reads
For high-leverage agent flows (the most common being "should I underwrite this merchant"), a **composite endpoint** bundles multiple axes into one envelope:
* [`GET /v2/underwriting-signals/:merchantId`](/api-reference/public/underwriting-signals) — Schema B latest-per-lender + Schema C merchant-wide rollup + CreditTier upgrade preview + `summary.anchorTier` + `summary.reliabilityScore`
Cuts 3-4 round trips to 1 for the load-bearing lender-agent decision.
## MCP tool surface
Consumer agents (ChatGPT, Claude, OpenAI Agents SDK) consume the trust fabric via the [droplinked-mcp server](/agentic/mcp-server):
| Tool | Wraps |
| ---------------------------- | ----------------------------------------------------------------------------------- |
| `verify_brand_attestation` | Schema A read |
| `verify_credit_risk` | Schema B read |
| `verify_repayment_history` | Schema C read |
| `verify_cross_attestation` | Schema D read |
| `get_trust_dossier` | Multi-axis composite |
| `verify_lender` | Lender registry lookup |
| `get_lender_history` | Lender lifecycle timeline (REGISTERED / STATUS\_CHANGED / metadata edits, redacted) |
| `verify_methodology` | Methodology registry lookup by `(lenderId, hash)` |
| `get_methodology_timeline` | Methodology lifecycle timeline (REGISTERED / SUPERSEDED / REVOKED, redacted) |
| `recommend_lender` | Lender routing |
| `recommend_service_provider` | Service-provider routing |
## Mainnet
Currently on Base Sepolia testnet. Mainnet flip gated on the KMS-backed signer migration (operator-side runbook). All `/v2/attestations/*` endpoints return the active chain in their response so consumers can verify the on-chain receipt on the right [easscan domain](https://easscan.org).
# Getting started
Source: https://docs.droplinked.com/developers/getting-started
Partner-facing API surfaces — the InventoryOS Public API, the Lending Application API, and KYB cohort routing. Auth, scopes, and your first call.
**BETA — APIs in this section may change.** They power partner integrations (lenders, 3PLs, agentic buyers, KYB providers) and are in active iteration. Pin versions in production code and watch the changelog.
The **Developers** section documents the partner-facing surfaces of the Droplinked platform — the read-and-write API contracts that lenders, 3PLs, agentic buyers, KYB providers, and large merchants integrate against. These complement the public commerce API in the [API Reference](/api-reference/introduction); pick this section if you're building an integration that consumes inventory data, submits a lending application, or routes around the merchant onboarding cohorts.
## What you can do here
Eight inventory data streams (catalog, inventory level, sell-through, return rate, pricing,
attribution, settlement, provenance) read via REST + delivered via webhook subscriptions.
Submit, update, and review merchant lending applications. State machine + per-cohort doc
checklist + CredibleX referral model.
The cohort taxonomy that routes a merchant's KYB sources, MoR entity, and lending tier.
## Authentication
There are **two distinct auth schemes** in this section — match the scheme to the surface:
| Surface | Auth scheme | How it's sent |
| ------------------------------------------------------ | ----------------------------------------------------- | ------------------------------------ |
| **InventoryOS Public API** (`/v1/api/inventory-os/*`) | Merchant-scoped **API key** | `x-droplinked-api-key: ` header |
| **Lending Application API** (`/lending-application/*`) | Merchant **JWT** (the standard Droplinked auth token) | `Authorization: Bearer ` header |
| **KYB status** (`/kyb/merchant/:merchantId/status`) | Merchant **JWT** | `Authorization: Bearer ` header |
### InventoryOS API keys
InventoryOS keys are bound to a single merchant's shop. Send the key in the
`x-droplinked-api-key` header. (`x-gravitee-api-key` and the `?api_key=` query parameter are
also accepted for gateway compatibility.)
```bash theme={null}
curl https://apiv3.droplinked.com/v1/api/inventory-os/streams/catalog \
-H "x-droplinked-api-key: REPLACE_ME"
```
Every row returned is filtered server-side to the merchant the key is bound to — a key
physically cannot read another merchant's data, regardless of the scope grant.
### Scopes (InventoryOS API)
| Scope | Grants |
| ---------------------------------- | --------------------------------------------------------- |
| `inventory-os:streams:read` | Read any InventoryOS stream (paginated reads + snapshots) |
| `inventory-os:subscriptions:read` | List existing webhook subscriptions |
| `inventory-os:subscriptions:write` | Create and delete webhook subscriptions |
Scopes are additive and bound to the key at creation time. A key with `*` is granted all
scopes. Issue separate keys for separate integration responsibilities — don't mint one
omni-scope key.
## Rate limits
InventoryOS rate limits are **per-stream**, applied at the handler level plus an in-process
per-(key, stream) counter so one integration polling `catalog` can't starve another hitting
`settlement` on the same key.
| Stream class | Reads/min/key | Max subscriptions/key |
| ------------------------------------------------ | ------------- | --------------------- |
| `catalog`, `pricing` | 1000 | 10 |
| `inventory-level`, `sell-through`, `return-rate` | 500 | 5 |
| `attribution`, `settlement`, `provenance` | 250 | 3 |
Exact per-stream limits are published live on the unauthenticated
[manifest endpoint](/developers/inventory-os-api/overview#discovery-manifest).
## Pagination
InventoryOS list endpoints use **opaque forward-only cursor pagination**. Pass `?limit=` (1–200,
default 50) and follow `nextCursor` until it's `null`.
```bash theme={null}
curl "https://apiv3.droplinked.com/v1/api/inventory-os/streams/catalog?limit=50&cursor=NjY3ZWUyMzJhYjY..." \
-H "x-droplinked-api-key: REPLACE_ME"
```
Response envelope:
```json theme={null}
{
"streamType": "catalog",
"merchantId": "65df8abc123…",
"data": [ /* page of rows */ ],
"count": 50,
"nextCursor": "NjY3ZWUyMzJhYjY..."
}
```
The cursor is the base64url-encoded id of the last row on the page — it's opaque, don't parse
it, and don't reuse cursors across keys or streams.
## Environments
| Environment | Base URL |
| ----------- | --------------------------------- |
| Production | `https://apiv3.droplinked.com` |
| Development | `https://apiv3dev.droplinked.com` |
Data is isolated per environment. Build and test against development; promote to production with
production-environment keys.
## Issuing keys
InventoryOS API keys are minted through the API-key management surface:
```
POST /shops/v2/api-key
```
This is the same ApiKeyV2 surface that powers other partner integrations; bind the
`inventory-os:*` scopes you need at creation time. For Tier-1 CredibleX referral access and
Tier-3 vault decisioning, contact
[partners@droplinked.com](mailto:partners@droplinked.com).
The eight streams, the consumer surfaces, and the data-layer thesis.
# InventoryOS overview
Source: https://docs.droplinked.com/developers/inventory-os-api/overview
The headless inventory data layer — eight streams × five consumer surfaces, exposed via REST + webhook subscriptions.
**BETA.** Stream shapes, cursor encoding, and webhook event names may change before GA. The surface is gated by the `INVENTORY_OS_API_ENABLED` server flag.
**InventoryOS** is Droplinked's headless inventory data layer. The same SKU + provenance +
stock-level + sell-through record simultaneously serves five orthogonal consumer surfaces — a
lending underwriter, an agentic buyer surface, a merchant program builder, a 3PL partner, and a
compliance gate — without the surfaces ever speaking to each other.
The API is the partner-facing surface of that layer. You read streams, register for webhook
deliveries, and stitch InventoryOS into your own systems.
## The eight streams
Product records — titles, slugs, type, status, tags. Sourced from `ProductV2`.
Stock per SKU. Quantity + available, sourced from `ProductSkuV2`.
Per-sale commission events — sale amount, commission, status. Sourced from `CommissionEventV2`.
Refunded + cancelled orders, sourced from `OrderV2`.
List price, raw price, royalty per SKU. Sourced from `ProductSkuV2`.
Affiliate attribution records — link token, agent, session. Sourced from `AffiliateAttributionRecord`.
Order payment + financial detail projection. Sourced from `OrderV2`.
Onchain NFT recording per product (chain, contract, token, tx). Sourced from `ProductV2.nftRecording`.
## The five consumer surfaces
| Surface | Streams consumed | Typical integrator |
| ---------------------------- | --------------------------- | --------------------------------------------------------- |
| **Lending underwriter** | 2, 3, 4, 7, 8 | CredibleX, US LFI corridor lenders, Tier-3 vault partners |
| **Agentic buyer discovery** | 1, 2, 5, 8 | MCP clients, ACP-feed consumers, x402 buyers |
| **Merchant program builder** | 1, 5, 6, 7 | Affiliate engine, dynamic-pricing, lending eligibility UX |
| **3PL data partner** | 2, 3, 4 outbound; 8 inbound | Stord, Flowspace, ShipHero |
| **Compliance gate** | 6, 7, 8 | FTC engine, 1099-NEC export, AML/Travel Rule |
You only need the streams that map to your surface. A lender doesn't need attribution (6); a 3PL
doesn't need settlement (7); an agentic buyer doesn't need return-rate (4).
## Why the data layer is multi-purpose
The same canonical record powers multiple surfaces because each stream is a per-surface read
projection over the underlying merchant resources (`ProductV2`, `ProductSkuV2`, `OrderV2`,
`CommissionEventV2`, `AffiliateAttributionRecord`). A merchant updates a SKU once — the catalog
read model reflects it, and the same row underpins the ACP feed, the lending collateral catalog,
the affiliate program builder, the 3PL partner sync, and the compliance audit trail.
## Auth + scopes (quick recap)
* All stream + subscription endpoints require an InventoryOS API key sent as
`x-droplinked-api-key: `.
* Read endpoints require the `inventory-os:streams:read` scope.
* Listing subscriptions requires `inventory-os:subscriptions:read`; creating/deleting them
requires `inventory-os:subscriptions:write`.
See [Getting started](/developers/getting-started) for full auth, scopes, rate limits, and
cursor pagination.
## Base path
```
https://apiv3.droplinked.com/v1/api/inventory-os
```
Every stream read lives under `/streams/:streamType`; webhook subscriptions are managed under
`/streams/:streamType/subscribe` and `/streams/:streamType/subscriptions`.
## Endpoints
| Operation | Method + path | Scope |
| -------------------------------------- | ------------------------------------------------------------------- | ---------------------------------- |
| Read a stream (paginated) | `GET /v1/api/inventory-os/streams/:streamType` | `inventory-os:streams:read` |
| Cached snapshot (first 200 rows) | `GET /v1/api/inventory-os/streams/:streamType/snapshot` | `inventory-os:streams:read` |
| Subscribe a webhook to a stream | `POST /v1/api/inventory-os/streams/:streamType/subscribe` | `inventory-os:subscriptions:write` |
| List a key's subscriptions on a stream | `GET /v1/api/inventory-os/streams/:streamType/subscriptions` | `inventory-os:subscriptions:read` |
| Delete a subscription | `DELETE /v1/api/inventory-os/streams/:streamType/subscriptions/:id` | `inventory-os:subscriptions:write` |
| Discovery manifest | `GET /v1/api/inventory-os/manifest` | none (public) |
`:streamType` is one of: `catalog`, `inventory-level`, `sell-through`, `return-rate`, `pricing`,
`attribution`, `settlement`, `provenance`. An unknown stream type returns a 404-class error.
## Webhook subscriptions
Most integrators subscribe rather than poll. Register a callback URL on a specific stream; we
deliver HMAC-SHA256-signed JSON POSTs when the stream's rows change for your merchant.
See [Webhook subscriptions](/developers/inventory-os-api/subscriptions) for the create flow,
event vocabulary, and the one-time HMAC signing secret.
## Discovery manifest
`GET /v1/api/inventory-os/manifest` is intentionally **unauthenticated** so SDK generators,
agent crawlers, and vector indexers can read the API shape at build time. It returns the stream
vocabulary, per-stream rate limits, the scope vocabulary, the auth header names, and a pointer
to the OpenAPI spec (`/api-doc-json`). No merchant data ever surfaces there.
```bash theme={null}
curl https://apiv3.droplinked.com/v1/api/inventory-os/manifest
```
The product record — titles, slugs, status.
# Stream 6 — Attribution
Source: https://docs.droplinked.com/developers/inventory-os-api/streams/attribution
Affiliate attribution records — link token, agent, session — projected from AffiliateAttributionRecord.
**BETA.** The attribution stream projects affiliate attribution records from `AffiliateAttributionRecord` (joined to the merchant's products).
The **attribution stream** is the affiliate attribution record: the link token that drove the
visit, the affiliate agent, the product, the session, and the attribution-window expiry. Rows are
scoped to the products owned by the API key's merchant.
## Endpoint
```
GET /v1/api/inventory-os/streams/attribution
```
**Query parameters:**
| Param | Type | Notes |
| -------- | -------- | ---------------------------------------------------- |
| `limit` | int | 1–200, default 50 |
| `cursor` | string | Opaque pagination token |
| `since` | ISO-8601 | Optional; records created on or after this timestamp |
## Row shape
```json theme={null}
{
"id": "665fattr123…",
"linkToken": "lnk_4xy…",
"agentId": "665fagt123…",
"productId": "665f8abc123…",
"sessionId": "sess_a1b2c3…",
"expiresAt": "2026-07-01T11:55:42.000Z",
"createdAt": "2026-06-01T11:55:42.000Z"
}
```
| Field | Meaning |
| ----------- | ------------------------------------------------------------------------- |
| `id` | `AffiliateAttributionRecord` id; pagination cursor advances on this field |
| `linkToken` | The affiliate link token that produced the attribution |
| `agentId` | The affiliate agent credited |
| `productId` | The attributed product (join key to the catalog stream) |
| `sessionId` | Visitor session |
| `expiresAt` | Attribution-window expiry |
## Response envelope
```json theme={null}
{
"streamType": "attribution",
"merchantId": "65df8abc123…",
"data": [ /* rows */ ],
"count": 50,
"nextCursor": "NjY1ZmF0dHIxMjM..."
}
```
## curl
```bash theme={null}
curl "https://apiv3.droplinked.com/v1/api/inventory-os/streams/attribution?since=2026-05-31T00:00:00.000Z&limit=200" \
-H "x-droplinked-api-key: REPLACE_ME"
```
## Affiliate-engine usage
The attribution stream pairs with the sell-through stream (3, commission events) and the
settlement stream (7, order payments) to reconcile which agent and link drove each settled sale.
Use `since` to pull only the new attributions since the last sync.
## Rate limits
Low-budget class — **250 reads/min/key**, up to **3 subscriptions/key**.
## Related
* [Sell-through stream](/developers/inventory-os-api/streams/sell-through) — commission events per merchant
* [Settlement stream](/developers/inventory-os-api/streams/settlement) — order payment + financial detail
* [Webhook subscriptions](/developers/inventory-os-api/subscriptions) — `attribution.changed` events
# Stream 1 — Catalog
Source: https://docs.droplinked.com/developers/inventory-os-api/streams/catalog
Product records — title, slug, type, status, tags — projected from ProductV2.
**BETA.** The catalog stream is the foundational stream — every other stream references products from here.
The **catalog stream** projects the product record per product from `ProductV2`: title, slug,
type, status, visibility, purchasability, tags, and Google product category. Rows are scoped to
the shops owned by the API key's merchant.
## Endpoint
```
GET /v1/api/inventory-os/streams/catalog
```
**Query parameters:**
| Param | Type | Notes |
| -------- | -------- | ----------------------------------------------------------------- |
| `limit` | int | 1–200, default 50 |
| `cursor` | string | Opaque pagination token from the previous response's `nextCursor` |
| `since` | ISO-8601 | Optional; only products updated on or after this timestamp |
There is no `shop_id` query parameter — rows are already scoped to the key's merchant
(across all shops the merchant owns). Tenant isolation is enforced server-side regardless of
query.
## Row shape
```json theme={null}
{
"id": "665f8abc123…",
"shopId": "65df8abc123…",
"title": "Limited Edition Drop Tee",
"slug": "limited-edition-drop-tee",
"type": "DIGITAL",
"status": "PUBLISHED",
"isVisible": true,
"isPurchasable": true,
"tags": ["apparel", "limited"],
"googleProductCategory": "Apparel & Accessories",
"updatedAt": "2026-06-01T12:00:00.000Z",
"createdAt": "2026-05-01T12:00:00.000Z"
}
```
| Field | Meaning |
| ----------------------------- | ------------------------------------------------------------ |
| `id` | `ProductV2` id; the pagination cursor advances on this field |
| `shopId` | Owning shop |
| `type` / `status` | Product type and lifecycle status |
| `isVisible` / `isPurchasable` | Storefront flags |
| `googleProductCategory` | Feed taxonomy category |
## Response envelope
```json theme={null}
{
"streamType": "catalog",
"merchantId": "65df8abc123…",
"data": [ /* rows */ ],
"count": 50,
"nextCursor": "NjY1ZjhhYmMxMjM..."
}
```
## curl
```bash theme={null}
curl "https://apiv3.droplinked.com/v1/api/inventory-os/streams/catalog?limit=50" \
-H "x-droplinked-api-key: REPLACE_ME"
```
## Snapshot
For a cached point-in-time view of the first 200 rows (TTL controlled by
`INVENTORY_OS_API_SNAPSHOT_CACHE_TTL_SEC`, default 300s):
```bash theme={null}
curl "https://apiv3.droplinked.com/v1/api/inventory-os/streams/catalog/snapshot" \
-H "x-droplinked-api-key: REPLACE_ME"
```
## Cursor pagination
Pages return `nextCursor` until the stream is exhausted (`null`). Cursors are opaque — don't
parse them. Use `since` for incremental syncs instead of walking the full feed each time.
```bash theme={null}
# Walk forward by feeding nextCursor back as ?cursor= until it returns null.
curl "https://apiv3.droplinked.com/v1/api/inventory-os/streams/catalog?limit=200&cursor=$NEXT" \
-H "x-droplinked-api-key: REPLACE_ME"
```
## Rate limits
Catalog reads are in the high-budget class — **1000 reads/min/key**. Use `since` + a larger
`limit` for incremental syncs; full crawls should be hourly or less frequent.
## Related
* [Inventory level stream](/developers/inventory-os-api/streams/inventory-level) — stock per SKU
* [Pricing stream](/developers/inventory-os-api/streams/pricing) — list price + royalty per SKU
* [Provenance stream](/developers/inventory-os-api/streams/provenance) — onchain NFT recording per product
* [Webhook subscriptions](/developers/inventory-os-api/subscriptions) — receive `catalog.changed` events
# Stream 2 — Inventory level
Source: https://docs.droplinked.com/developers/inventory-os-api/streams/inventory-level
Stock per SKU — quantity + available — projected from ProductSkuV2.
**BETA.** The inventory-level stream projects current stock per SKU from `ProductSkuV2` (joined to the merchant's products).
The **inventory-level stream** is the per-SKU stock record: quantity and available, plus the
warehouse external id when the SKU is synced from a 3PL. Rows are scoped to the products owned by
the API key's merchant.
## Endpoint
```
GET /v1/api/inventory-os/streams/inventory-level
```
**Query parameters:**
| Param | Type | Notes |
| -------- | -------- | ------------------------------------------------- |
| `limit` | int | 1–200, default 50 |
| `cursor` | string | Opaque pagination token |
| `since` | ISO-8601 | Optional; SKUs updated on or after this timestamp |
## Row shape
```json theme={null}
{
"skuId": "665fsku123…",
"productId": "665f8abc123…",
"type": "DIGITAL",
"inventoryQuantity": 150,
"inventoryAvailable": 142,
"warehouseExternalId": "stord-warehouse-atl-1",
"updatedAt": "2026-06-01T12:00:00.000Z"
}
```
| Field | Meaning |
| --------------------- | ---------------------------------------------------------------------------------- |
| `skuId` | `ProductSkuV2` id |
| `productId` | Parent product (join key to the catalog stream) |
| `inventoryQuantity` | Total stock (`inventory.quantity`) |
| `inventoryAvailable` | Sellable stock (`inventory.available`) |
| `warehouseExternalId` | 3PL/warehouse external id when synced from a fulfillment partner; `null` otherwise |
## Response envelope
```json theme={null}
{
"streamType": "inventory-level",
"merchantId": "65df8abc123…",
"data": [ /* rows */ ],
"count": 50,
"nextCursor": "NjY1ZnNrdTEyMw..."
}
```
## curl
```bash theme={null}
curl "https://apiv3.droplinked.com/v1/api/inventory-os/streams/inventory-level?limit=100" \
-H "x-droplinked-api-key: REPLACE_ME"
```
## Lender-facing usage
The lending underwriter uses this stream as a collateral-valuation input. Combined with the
provenance stream (8) and PSP-confirmed settlement stream (7), 3PL-attested custody gets the
underwriter materially off the lending rate. **Subscribe to webhooks** (vs polling) for active
lender integrations.
## Rate limits
Mid-budget class — **500 reads/min/key**, up to **5 subscriptions/key**.
## Related
* [Catalog stream](/developers/inventory-os-api/streams/catalog) — product definitions referenced here via `productId`
* [Sell-through stream](/developers/inventory-os-api/streams/sell-through) — sale events per merchant
* [Provenance stream](/developers/inventory-os-api/streams/provenance) — onchain recording per product
* [Webhook subscriptions](/developers/inventory-os-api/subscriptions) — `inventory.changed` events
# Stream 5 — Pricing
Source: https://docs.droplinked.com/developers/inventory-os-api/streams/pricing
List price, raw price, and royalty per SKU — projected from ProductSkuV2.
**BETA.** The pricing stream projects per-SKU pricing fields from `ProductSkuV2` (joined to the merchant's products).
The **pricing stream** is the per-SKU price record: list price, raw price, and royalty percent.
Rows are scoped to the products owned by the API key's merchant.
## Endpoint
```
GET /v1/api/inventory-os/streams/pricing
```
**Query parameters:**
| Param | Type | Notes |
| -------- | -------- | ------------------------------------------------- |
| `limit` | int | 1–200, default 50 |
| `cursor` | string | Opaque pagination token |
| `since` | ISO-8601 | Optional; SKUs updated on or after this timestamp |
## Row shape
```json theme={null}
{
"skuId": "665fsku123…",
"productId": "665f8abc123…",
"listPriceUsd": 29.99,
"rawPriceUsd": 24.0,
"royaltyPercent": 10,
"updatedAt": "2026-06-01T12:00:00.000Z"
}
```
| Field | Meaning |
| ---------------- | ------------------------------------------------ |
| `skuId` | `ProductSkuV2` id |
| `productId` | Parent product (join key to the catalog stream) |
| `listPriceUsd` | List sell price (`price`) |
| `rawPriceUsd` | Raw/base price (`rawPrice`); `null` when unset |
| `royaltyPercent` | Royalty percentage on the SKU; `null` when unset |
## Response envelope
```json theme={null}
{
"streamType": "pricing",
"merchantId": "65df8abc123…",
"data": [ /* rows */ ],
"count": 50,
"nextCursor": "NjY1ZnNrdTEyMw..."
}
```
## curl
```bash theme={null}
curl "https://apiv3.droplinked.com/v1/api/inventory-os/streams/pricing?limit=200" \
-H "x-droplinked-api-key: REPLACE_ME"
```
## Lender / margin usage
Cross-referenced with the catalog stream (1) and the sell-through stream (3), the underwriter
derives a gross-margin trend (`listPriceUsd` vs `rawPriceUsd`). Use `since` to pull only the
SKUs that changed since the last sync.
## Rate limits
High-budget class — **1000 reads/min/key**, up to **10 subscriptions/key**.
## Related
* [Catalog stream](/developers/inventory-os-api/streams/catalog) — product reference via `productId`
* [Inventory level stream](/developers/inventory-os-api/streams/inventory-level) — stock per SKU
* [Webhook subscriptions](/developers/inventory-os-api/subscriptions) — `pricing.changed` events
# Stream 8 — Provenance
Source: https://docs.droplinked.com/developers/inventory-os-api/streams/provenance
Onchain NFT recording per product — chain, contract, token, transaction — projected from ProductV2.nftRecording.
**BETA.** In v1 the provenance stream projects `ProductV2.nftRecording` rows. When the unified multi-chain attestation model lands, this method swaps out without changing the partner-facing shape.
The **provenance stream** is the onchain recording per product: the chain, contract address,
token id, and transaction hash for products that have been recorded onchain. Rows are scoped to
the shops owned by the API key's merchant; only products that carry an `nftRecording` are
returned.
## Endpoint
```
GET /v1/api/inventory-os/streams/provenance
```
**Query parameters:**
| Param | Type | Notes |
| -------- | -------- | ----------------------------------------------------- |
| `limit` | int | 1–200, default 50 |
| `cursor` | string | Opaque pagination token |
| `since` | ISO-8601 | Optional; products updated on or after this timestamp |
## Row shape
```json theme={null}
{
"productId": "665f8abc123…",
"title": "Limited Edition Drop Tee",
"chain": "base",
"contractAddress": "0xContractAddress…",
"tokenId": "42",
"txHash": "0xtxhash…",
"updatedAt": "2026-06-01T12:00:00.000Z"
}
```
| Field | Meaning |
| ----------------- | ------------------------------------------------------------------------- |
| `productId` | `ProductV2` id; pagination cursor advances on this field |
| `title` | Product title |
| `chain` | Chain the product was recorded on (`nftRecording.chain`); `null` if unset |
| `contractAddress` | Onchain contract address; `null` if unset |
| `tokenId` | Token id within the contract; `null` if unset |
| `txHash` | Recording transaction hash; `null` if unset |
## Response envelope
```json theme={null}
{
"streamType": "provenance",
"merchantId": "65df8abc123…",
"data": [ /* rows */ ],
"count": 50,
"nextCursor": "NjY1ZjhhYmMxMjM..."
}
```
## curl
```bash theme={null}
curl "https://apiv3.droplinked.com/v1/api/inventory-os/streams/provenance?limit=200" \
-H "x-droplinked-api-key: REPLACE_ME"
```
## Verification
A recorded product can be independently verified onchain: look up the `contractAddress` +
`tokenId` on the named `chain` — Droplinked does not need to be online for verification. This is
the cryptographic-unforgeability property that gives this stream the highest trust weight in the
lender signal hierarchy.
## Lender-facing usage
Combined with PSP-confirmed settlement (Stream 7) and inventory level (Stream 2), the onchain
recording is the highest-trust collateral signal in the underwriting stack. Use `since` to pull
only newly-recorded products.
## Rate limits
Low-budget class — **250 reads/min/key**, up to **3 subscriptions/key**.
## Related
* [Catalog stream](/developers/inventory-os-api/streams/catalog) — product reference via `productId`
* [Inventory level stream](/developers/inventory-os-api/streams/inventory-level) — stock per SKU
* [Settlement stream](/developers/inventory-os-api/streams/settlement) — pair with provenance for lender underwriting
* [Webhook subscriptions](/developers/inventory-os-api/subscriptions) — `provenance.changed` events
# Stream 4 — Return rate
Source: https://docs.droplinked.com/developers/inventory-os-api/streams/return-rate
Refunded + cancelled orders — projected from OrderV2.
**BETA.** The return-rate stream projects refunded and cancelled orders from `OrderV2` (status in `REFUNDED`, `CANCELLED`).
The **return-rate stream** emits the merchant's refunded and cancelled orders. Compute the return
rate client-side by comparing this stream's volume against total order / sell-through volume.
Rows are scoped to the shops owned by the API key's merchant.
## Endpoint
```
GET /v1/api/inventory-os/streams/return-rate
```
**Query parameters:**
| Param | Type | Notes |
| -------- | -------- | --------------------------------------------------- |
| `limit` | int | 1–200, default 50 |
| `cursor` | string | Opaque pagination token |
| `since` | ISO-8601 | Optional; orders updated on or after this timestamp |
## Row shape
```json theme={null}
{
"id": "665ford123…",
"orderNumber": "DRP-100245",
"shopId": "65df8abc123…",
"status": "REFUNDED",
"orderDate": "2026-05-28T09:14:00.000Z",
"updatedAt": "2026-06-01T12:00:00.000Z"
}
```
| Field | Meaning |
| ------------- | ------------------------------------------------------ |
| `id` | `OrderV2` id; pagination cursor advances on this field |
| `orderNumber` | Human-readable order number |
| `status` | `REFUNDED` or `CANCELLED` |
| `orderDate` | When the order was originally placed |
## Response envelope
```json theme={null}
{
"streamType": "return-rate",
"merchantId": "65df8abc123…",
"data": [ /* rows */ ],
"count": 50,
"nextCursor": "NjY1Zm9yZDEyMw..."
}
```
## curl
```bash theme={null}
curl "https://apiv3.droplinked.com/v1/api/inventory-os/streams/return-rate?limit=200" \
-H "x-droplinked-api-key: REPLACE_ME"
```
## Lender-facing usage
Combined with Stream 3 (sell-through), the underwriter derives the net-revenue forecast and an
LGD input. The return-rate stream isolates the refund/cancel numerator so a working-capital line
can be priced against it.
## Rate limits
Mid-budget class — **500 reads/min/key**, up to **5 subscriptions/key**.
## Related
* [Sell-through stream](/developers/inventory-os-api/streams/sell-through) — sale events per merchant
* [Settlement stream](/developers/inventory-os-api/streams/settlement) — order payment + financial detail
* [Webhook subscriptions](/developers/inventory-os-api/subscriptions) — `return-rate.changed` events
# Stream 3 — Sell-through
Source: https://docs.droplinked.com/developers/inventory-os-api/streams/sell-through
Per-sale commission events — sale amount, commission, status — projected from CommissionEventV2.
**BETA.** The sell-through stream projects per-sale commission events from `CommissionEventV2`. Each row is one sale, not a daily rollup — aggregate client-side over the window you care about.
The **sell-through stream** emits one row per sale (commission event) for the merchant: the
product, order, agent, sale amount, commission amount, and status. The lending underwriter
consumes a trailing window of these as the cash-flow / velocity signal; the affiliate program
builder consumes the same rows to drive payout and tiering.
## Endpoint
```
GET /v1/api/inventory-os/streams/sell-through
```
**Query parameters:**
| Param | Type | Notes |
| -------- | -------- | --------------------------------------------------- |
| `limit` | int | 1–200, default 50 |
| `cursor` | string | Opaque pagination token |
| `since` | ISO-8601 | Optional; events created on or after this timestamp |
## Row shape
```json theme={null}
{
"id": "665fcomm123…",
"productId": "665f8abc123…",
"orderId": "665ford123…",
"agentId": "665fagt123…",
"saleAmountUsd": 99.0,
"commissionAmountUsd": 9.9,
"status": "CONFIRMED",
"createdAt": "2026-06-01T12:00:00.000Z"
}
```
| Field | Meaning |
| ----------------------------------- | ---------------------------------------------------------------- |
| `id` | `CommissionEventV2` id; pagination cursor advances on this field |
| `productId` / `orderId` / `agentId` | Join keys to product, order, and affiliate agent |
| `saleAmountUsd` | Sale value in USD |
| `commissionAmountUsd` | Commission accrued on the sale, in USD |
| `status` | Commission event status |
## Response envelope
```json theme={null}
{
"streamType": "sell-through",
"merchantId": "65df8abc123…",
"data": [ /* rows */ ],
"count": 50,
"nextCursor": "NjY1ZmNvbW0xMjM..."
}
```
## curl
```bash theme={null}
curl "https://apiv3.droplinked.com/v1/api/inventory-os/streams/sell-through?since=2026-03-01T00:00:00.000Z&limit=200" \
-H "x-droplinked-api-key: REPLACE_ME"
```
## Lender-facing usage
Aggregate `saleAmountUsd` over a trailing window (e.g. 90 days) client-side to derive the
net-revenue / velocity signal. Combined with Stream 4 (return rate) and Stream 7 (settlement),
the underwriter derives an LGD input. Use `since` to pull only the new events since the last
sync.
## Rate limits
Mid-budget class — **500 reads/min/key**, up to **5 subscriptions/key**.
## Related
* [Return-rate stream](/developers/inventory-os-api/streams/return-rate) — refunded + cancelled orders
* [Settlement stream](/developers/inventory-os-api/streams/settlement) — order payment + financial detail
* [Webhook subscriptions](/developers/inventory-os-api/subscriptions) — `sell-through.changed` events
# Stream 7 — Settlement
Source: https://docs.droplinked.com/developers/inventory-os-api/streams/settlement
Order payment + financial-detail projection — provider, status, amount — projected from OrderV2.
**BETA — Critical stream for lenders.** Settlement is the canonical revenue signal for lending underwriting. In v1 the stream exposes the `OrderV2` payment + financial-detail shape directly; a normalized cross-PSP `UnifiedTransaction` projection is planned and will swap in without changing this partner-facing shape.
The **settlement stream** projects per-order payment and financial detail from `OrderV2`: the
order, its status, the payment provider and payment status, the USD amount, the currency, and the
capture timestamp. The lending underwriter consumes a trailing window of these as the revenue
signal. Rows are scoped to the shops owned by the API key's merchant.
## Endpoint
```
GET /v1/api/inventory-os/streams/settlement
```
**Query parameters:**
| Param | Type | Notes |
| -------- | -------- | --------------------------------------------------- |
| `limit` | int | 1–200, default 50 |
| `cursor` | string | Opaque pagination token |
| `since` | ISO-8601 | Optional; orders updated on or after this timestamp |
## Row shape
```json theme={null}
{
"orderId": "665ford123…",
"orderNumber": "DRP-100245",
"status": "COMPLETED",
"paymentProvider": "stripe",
"paymentStatus": "CAPTURED",
"amountUsd": 124.96,
"currency": "USD",
"capturedAt": "2026-06-01T12:01:33.000Z",
"updatedAt": "2026-06-01T12:01:33.000Z"
}
```
| Field | Meaning |
| ----------------- | --------------------------------------------------------------------- |
| `orderId` | `OrderV2` id; pagination cursor advances on this field |
| `orderNumber` | Human-readable order number |
| `status` | Order status |
| `paymentProvider` | PSP that processed the payment (`payment.provider`); `null` if unset |
| `paymentStatus` | PSP-side payment status (`payment.status`); `null` if unset |
| `amountUsd` | Order total in USD (`financialDetails.totalUsd`); `null` if unset |
| `currency` | Settlement currency (`financialDetails.currency`); `null` if unset |
| `capturedAt` | When the payment was captured (`payment.capturedAt`); `null` if unset |
The v1 shape exposes the `OrderPayment` fields verbatim so partners can build against the
surface today. The forthcoming `UnifiedTransaction` projection will normalize per-PSP shapes
(Stripe / PayPal / Bonum / Telr / Paymob) and add cohort + MoR-entity context — without changing
the field names a partner already reads.
## Response envelope
```json theme={null}
{
"streamType": "settlement",
"merchantId": "65df8abc123…",
"data": [ /* rows */ ],
"count": 50,
"nextCursor": "NjY1Zm9yZDEyMw..."
}
```
## curl
```bash theme={null}
curl "https://apiv3.droplinked.com/v1/api/inventory-os/streams/settlement?since=2026-05-01T00:00:00.000Z&limit=200" \
-H "x-droplinked-api-key: REPLACE_ME"
```
## Lender-facing usage
This is **the** revenue signal for lending decisions. Aggregate `amountUsd` over 30/60/90-day
windows client-side; combined with provenance (Stream 8) and inventory level (Stream 2), the
underwriter prices a working-capital line against confirmed, captured revenue. Use `since` for
incremental pulls and **subscribe to `settlement.changed`** for near-real-time integration.
## Rate limits
Low-budget class — **250 reads/min/key**, up to **3 subscriptions/key**. Settlement reads are
usually batch (nightly reconciliation); subscribe for real-time.
## Related
* [Sell-through stream](/developers/inventory-os-api/streams/sell-through) — commission events cross-check
* [Return-rate stream](/developers/inventory-os-api/streams/return-rate) — refunded + cancelled orders
* [KYB cohorts](/developers/kyb-cohorts) — cohort + MoR routing context (planned for the UnifiedTransaction projection)
* [Webhook subscriptions](/developers/inventory-os-api/subscriptions) — `settlement.changed` events
# Webhook subscriptions
Source: https://docs.droplinked.com/developers/inventory-os-api/subscriptions
HMAC-signed JSON POSTs — register a webhook URL per stream, verify the signature, manage subscriptions.
**BETA.** Subscriptions are per-stream. Creating and deleting them requires the `inventory-os:subscriptions:write` scope; listing requires `inventory-os:subscriptions:read`.
InventoryOS streams are best consumed via **webhooks**. Register a callback URL on a specific
stream; when the stream's rows change for your merchant, we deliver an HMAC-SHA256-signed JSON
POST to that URL.
Subscriptions are **explicitly per-stream** — a partner that wants both `catalog` and
`inventory-level` fanouts creates two subscriptions, one on each stream.
## Endpoints
| Operation | Endpoint | Scope |
| ------------------- | ------------------------------------------------------------------- | ---------------------------------- |
| Create subscription | `POST /v1/api/inventory-os/streams/:streamType/subscribe` | `inventory-os:subscriptions:write` |
| List subscriptions | `GET /v1/api/inventory-os/streams/:streamType/subscriptions` | `inventory-os:subscriptions:read` |
| Delete subscription | `DELETE /v1/api/inventory-os/streams/:streamType/subscriptions/:id` | `inventory-os:subscriptions:write` |
There is no update (`PATCH`) or get-by-id endpoint, and no DLQ/replay endpoint. **Rotation
\= delete + recreate** (a fresh `hmacSecret` is issued on every create).
## Create a subscription
```bash theme={null}
curl -X POST https://apiv3.droplinked.com/v1/api/inventory-os/streams/settlement/subscribe \
-H "x-droplinked-api-key: REPLACE_ME" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://example.com/webhooks/droplinked",
"events": ["settlement.changed"],
"description": "Lender ingestion: settlement changes for my shop"
}'
```
**Request body:**
| Field | Required | Notes |
| ------------- | -------- | --------------------------------------------------------------------------------------------- |
| `webhookUrl` | yes | HTTPS endpoint (plain HTTP is rejected). 8–2048 chars. |
| `events` | no | Subset of the stream's event vocabulary. Defaults to all of the stream's events when omitted. |
| `description` | no | Human-readable label (0–200 chars). |
The response includes an `hmacSecret` (returned **once** — store it; you'll need it for
signature verification). Treat it like a password.
```json theme={null}
{
"id": "665f…",
"apiKeyId": "apk_…",
"merchantId": "65df8abc123…",
"streamType": "settlement",
"webhookUrl": "https://example.com/webhooks/droplinked",
"events": ["settlement.changed"],
"description": "Lender ingestion: settlement changes for my shop",
"status": "ACTIVE",
"failureCount": 0,
"hmacSecret": "GENERATED_ONCE_DO_NOT_LOSE"
}
```
Each create counts against the per-stream subscription cap (e.g. 3 for `settlement`, 10 for
`catalog`). Exceeding the cap returns a `409 Conflict` — delete an existing subscription first.
## List subscriptions
```bash theme={null}
curl "https://apiv3.droplinked.com/v1/api/inventory-os/streams/settlement/subscriptions?status=ACTIVE" \
-H "x-droplinked-api-key: REPLACE_ME"
```
Optional `status` filter: `ACTIVE`, `PAUSED`, or `EXPIRED`. The `hmacSecret` is **never**
re-surfaced on reads.
```json theme={null}
{
"data": [
{
"id": "665f…",
"apiKeyId": "apk_…",
"merchantId": "65df8abc123…",
"streamType": "settlement",
"webhookUrl": "https://example.com/webhooks/droplinked",
"events": ["settlement.changed"],
"status": "ACTIVE",
"failureCount": 0
}
],
"total": 1
}
```
## Delete a subscription
```bash theme={null}
curl -X DELETE https://apiv3.droplinked.com/v1/api/inventory-os/streams/settlement/subscriptions/665f... \
-H "x-droplinked-api-key: REPLACE_ME"
```
```json theme={null}
{ "ok": true, "deletedId": "665f..." }
```
## Available events
Each stream emits exactly one event — `.changed` — when an underlying row mutates for
your merchant.
| Stream | Event |
| ----------------- | ---------------------- |
| `catalog` | `catalog.changed` |
| `inventory-level` | `inventory.changed` |
| `sell-through` | `sell-through.changed` |
| `return-rate` | `return-rate.changed` |
| `pricing` | `pricing.changed` |
| `attribution` | `attribution.changed` |
| `settlement` | `settlement.changed` |
| `provenance` | `provenance.changed` |
Subscribing to an event not in the target stream's vocabulary returns a `400 Bad Request`.
## HMAC verification (Node.js)
The signing secret (`hmacSecret`) is 32 random bytes, hex-encoded — the same shape as the
affiliate webhook secret, so a shared HMAC verifier works across both surfaces. Verify the
signature on every delivery; never trust a payload without verification.
```ts theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";
import type { Request, Response } from "express";
const HMAC_SECRET = process.env.DROPLINKED_WEBHOOK_SECRET!; // the hmacSecret from create
export function verifySignature(req: Request): boolean {
const signature = req.header("X-Droplinked-Signature");
if (!signature) return false;
const rawBody = (req as Request & { rawBody?: string }).rawBody ?? "";
const expected = createHmac("sha256", HMAC_SECRET).update(rawBody).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(signature, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
export function handleWebhook(req: Request, res: Response) {
if (!verifySignature(req)) {
return res.status(401).send("invalid signature");
}
// ...process the event idempotently
res.status(200).send("ok");
}
```
Capture the **raw body** before any JSON parsing — most Node.js frameworks parse before
the handler runs. In Express, register a `bodyParser.raw` for the webhook path or set
`verify: (req, _, buf) => { req.rawBody = buf.toString("utf8"); }` on `bodyParser.json`.
## Reliability
Each subscription carries a `failureCount` and a `status` (`ACTIVE` / `PAUSED` / `EXPIRED`).
Repeated delivery failures increment `failureCount`; persistently failing subscriptions move out
of `ACTIVE`. Inspect a subscription's state via the list endpoint.
## Related
* [Getting started](/developers/getting-started) — auth, scopes, rate limits
* [InventoryOS overview](/developers/inventory-os-api/overview) — the eight streams
* [Settlement stream](/developers/inventory-os-api/streams/settlement) — the highest-value webhook surface for lenders
# KYB cohorts
Source: https://docs.droplinked.com/developers/kyb-cohorts
The eleven-cohort taxonomy that drives KYB sources, MoR entity, settlement routing, and lending-tier eligibility.
**BETA.** The cohort taxonomy is settled in the `KybCohort` enum; per-cohort PSP routing rolls out incrementally as each PSP onboarding flow goes live. The KYB orchestrator surface is gated by its server flag and returns `503` when disabled.
A merchant's **KYB cohort** answers four questions at once:
1. **Who is the Merchant of Record (MoR)?** — the merchant themselves, the PSP, or Shopsadiq Ltd
2. **What KYB sources feed the merchant's compliance record?** — PSP OAuth, MoR master KYB, merchant-direct OAuth, or Sumsub fallback
3. **Where does settlement land?** — merchant's bank, PSP master, or Shopsadiq master
4. **Which lending tier can underwrite this merchant?** — CredibleX (Tier 1, UAE), US LFI corridor (Tier 2), Tier-3 vault
Routing decisions in the platform (PSP onboarding offer set, doc checklist, lending tier
selection) switch on the cohort.
## The eleven cohorts
These are the 11 values of the `KybCohort` enum.
| Cohort | MoR | KYB source family | Status |
| --------------------------- | --------- | -------------------------------------------------- | ------ |
| `A_CONNECT_STRIPE` | merchant | `MERCHANT_DIRECT_OAUTH` (Stripe) + Sumsub fallback | live |
| `A_CONNECT_PAYPAL` | merchant | `MERCHANT_DIRECT_OAUTH` (PayPal) + Sumsub fallback | live |
| `A_CONNECT_TELR` | merchant | `MERCHANT_DIRECT_OAUTH` (Telr) + Sumsub fallback | live |
| `A_CONNECT_PAYMOB` | merchant | `MERCHANT_DIRECT_OAUTH` (Paymob) + Sumsub fallback | live |
| `A_CONNECT_BONUM` | merchant | merchant-direct + Sumsub fallback | live |
| `A_MOR_TELR_DIRECT` | Telr | `PSP_MOR` (Telr master KYB) | live |
| `A_MOR_BONUM_DIRECT` | Bonum | `PSP_MOR` (Bonum master KYB) | live |
| `A_MOR_SHOPSADIQ_VIA_TELR` | Shopsadiq | `PSP_MOR` (Shopsadiq master) + `PSP_OAUTH` (Telr) | live |
| `A_MOR_SHOPSADIQ_VIA_BONUM` | Shopsadiq | `PSP_MOR` (Shopsadiq master) + Bonum | live |
| `B_SUMSUB_FALLBACK` | merchant | `SUMSUB` (no PSP OAuth path available) | live |
| `C_MANUAL_OPERATOR` | merchant | `MANUAL` (operator-mediated) | live |
There is no `A_MOR_PAYPAL_DIRECT`, `A_MOR_PAYMOB_DIRECT`, `A_MOR_SHOPSADIQ_VIA_STRIPE`,
`A_MOR_SHOPSADIQ_VIA_PAYPAL`, or `A_MOR_SHOPSADIQ_VIA_PAYMOB` cohort in the current enum. Those
MoR variants are not yet defined — don't build against them.
## What the cohort decides
### MoR entity
The MoR entity (`KybMorEntity`) is one of: `MERCHANT`, `TELR`, `BONUM`, `SHOPSADIQ`, `PAYPAL`,
`STRIPE`, `PAYMOB`.
| Value | Meaning |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `MERCHANT` | Merchant is the regulated entity; PSP is gateway-only |
| `TELR` / `BONUM` / `PAYPAL` / `STRIPE` / `PAYMOB` | PSP carries MoR via its own aggregator product |
| `SHOPSADIQ` | Shopsadiq Ltd is the MoR under its Telr partnership (and analogous future PSP partnerships) |
The MoR entity is who bears chargeback risk, who carries the consumer disclosure obligation, and
whose bank account settlement lands in.
### KYB sources
A merchant's KYB record carries the source family that verified it. The source-type vocabulary
(`KybSourceType`) is:
| Source | When |
| ----------------------- | ---------------------------------------------------------------------------------------- |
| `MERCHANT_DIRECT_OAUTH` | Merchant connects their own PSP account via OAuth (Connect cohorts) |
| `PSP_OAUTH` | PSP OAuth from a master account (legacy alias for the OAuth path) |
| `PSP_MOR` | Master KYB inherited from a MoR aggregator (Telr-Direct, Bonum-Direct, Shopsadiq-master) |
| `SUMSUB` | Fallback when no PSP OAuth path is available — the merchant runs Sumsub directly |
| `MANUAL` | Operator-mediated verification (Cohort C) |
The `GET /kyb/merchant/:merchantId/status` response narrows this to its runtime `KybSource`
subset: `PSP_OAUTH`, `SUMSUB`, or `MANUAL`.
### Lending-tier eligibility
The cohort gates which lending tiers can underwrite a merchant. The credit-tier preview maps
cohorts to an eligibility floor:
| Tier | Lender | Cohort floor (per credit-tier mapping) |
| ---------- | ---------------------------------------- | --------------------------------------------------------- |
| **Tier 1** | CredibleX (UAE), planned US LFI corridor | `A_CONNECT_TELR` / `A_CONNECT_BONUM` / `A_CONNECT_PAYMOB` |
| **Tier 2** | US LFI corridor lenders | `A_CONNECT_STRIPE` / `A_CONNECT_PAYPAL` |
| **Tier 3** | Droplinked-direct FSRA vault | `A_MOR_*` cohorts |
| **Tier 0** | (no eligibility) | `B_SUMSUB_FALLBACK` / `C_MANUAL_OPERATOR` |
`operationalRisk` (`LOW` / `MEDIUM` / `HIGH`) and hard compliance flags (`OFAC_HIT`,
`SANCTIONS_HIT`, `JURISDICTION_RESTRICTED`) can cap or zero the resolved tier. See
[Credit-tier preview](/developers/lending-application-api/endpoints#credit-tier-preview).
### Doc checklist depth
| Cohort family | Doc count | Rationale |
| ----------------------- | ---------- | ------------------------------------------------------------------- |
| `A_MOR_SHOPSADIQ_VIA_*` | Light (3) | Sub-merchant inherits Shopsadiq master KYB |
| `A_MOR_*_DIRECT` | Medium (4) | Sub-merchant inherits PSP master KYB; add PSP settlement statements |
| `A_CONNECT_*` | Heavy (5) | Merchant is the regulated entity; full bank + tax pack |
| `B_SUMSUB_FALLBACK` | 4 | Sumsub-verified; bank + tax + trade cert + proof of address |
| `C_MANUAL_OPERATOR` | 3 | Operator-mediated; bank + tax + trade cert |
See the [Lending Application overview](/developers/lending-application-api/overview#per-cohort-doc-checklist)
for the exact document keys per cohort.
## Reading a merchant's KYB status
The merchant-facing KYB status endpoint is JWT-authenticated and ownership-checked (the
authenticated merchant must match the path `merchantId`); it requires the `PRODUCER` role.
```
GET /kyb/merchant/:merchantId/status
```
```bash theme={null}
curl https://apiv3.droplinked.com/kyb/merchant/665fmerch.../status \
-H "Authorization: Bearer "
```
### Response (`KybStatusDto`)
```json theme={null}
{
"status": "APPROVED",
"source": "PSP_OAUTH",
"pspVendor": "telr",
"verifiedAt": "2026-05-22T11:30:00.000Z",
"validUntil": "2027-05-22T11:30:00.000Z",
"reason": null
}
```
| Field | Meaning |
| --------------------------- | --------------------------------------------------------------------------------------------------- |
| `status` | `NOT_STARTED` / `PENDING` / `APPROVED` / `REJECTED` / `EXPIRED` (the `KybStatus` enum) |
| `source` | `PSP_OAUTH` / `SUMSUB` / `MANUAL` (present once resolved) |
| `pspVendor` | Populated when `source = PSP_OAUTH` (`stripe`/`paypal`/`paymob`/`telr`/`bonum`/`coinbase-commerce`) |
| `verifiedAt` / `validUntil` | Verification timestamp + expiry |
| `reason` | Diagnostic string when `PENDING` (e.g. "connect a PSP to unlock lending") |
This endpoint returns the **KYB status**, not the cohort. The cohort is carried on the
merchant's **lending application** record — read it via
`GET /lending-application/:id` or derive the per-cohort checklist via
`GET /lending-application/:id/checklist`.
## Running the KYB cascade
```
POST /kyb/merchant/:merchantId/initiate
```
Runs the PSP-OAuth → Sumsub → manual cascade. Body accepts an optional `jurisdiction` (ISO
3166-1 alpha-2 or a group key like `GCC`) and an optional `triggeredByShopId`. Same JWT +
`PRODUCER` role + ownership check as the status endpoint.
## How cohort is assigned
Cohort is assigned at PSP-onboarding time:
1. Merchant signs up and picks a jurisdiction.
2. PSP options are filtered to the merchant's jurisdiction.
3. The merchant picks a PSP + mode; the KYB orchestrator records the cohort and inherits the
appropriate KYB sources.
4. Subsequent lending applications and routing read this cohort.
## Related
* [Lending Application overview](/developers/lending-application-api/overview) — per-cohort doc checklist
* [Lending Application endpoints](/developers/lending-application-api/endpoints) — credit-tier preview maps cohort → tier
* [Settlement stream](/developers/inventory-os-api/streams/settlement) — order payment projection (cohort/MoR context planned for the UnifiedTransaction projection)
* [Getting started](/developers/getting-started) — auth schemes for each surface
# Lending Application API endpoints
Source: https://docs.droplinked.com/developers/lending-application-api/endpoints
Create, patch, submit, withdraw, decide — request / response shapes, curl examples.
**BETA.** The whole surface returns `404` unless `LENDING_PLATFORM_ENABLED=true`. Merchant routes use the merchant JWT; admin routes require a JWT with the `SUPER_ADMIN` role.
## Merchant-facing routes (`/lending-application`)
All routes require the merchant JWT (`Authorization: Bearer `) and are ownership-checked.
Write routes marked **KYB-gated** require an approved KYB record.
| Operation | Method + path | KYB-gated |
| ---------------------- | ---------------------------------------- | --------- |
| Create draft | `POST /lending-application` | yes |
| List my applications | `GET /lending-application?merchantId=…` | no |
| Get application | `GET /lending-application/:id` | no |
| Update application | `PATCH /lending-application/:id` | yes |
| Submit application | `POST /lending-application/:id/submit` | yes |
| Withdraw application | `POST /lending-application/:id/withdraw` | yes |
| Get document checklist | `GET /lending-application/:id/checklist` | no |
### Create draft
```
POST /lending-application
```
**Request** — `cohort` is optional; when omitted, the cohort is resolved server-side from the
merchant's KYB record. An explicit override is for operator-driven / Cohort-C manual drafts.
```json theme={null}
{ "cohort": "A_MOR_SHOPSADIQ_VIA_TELR" }
```
```bash theme={null}
curl -X POST https://apiv3.droplinked.com/lending-application \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{}'
```
The created draft carries (among other fields) `cohort`, `tier`, and `status: "DRAFT"`.
### Update application
```
PATCH /lending-application/:id
```
**Request** — all fields optional:
| Field | Type | Notes |
| ------------------------ | ------ | -------------------------------------------------------------- |
| `requestedAmountUsd` | number | 1000 – 10,000,000 |
| `requestedTermMonths` | int | 1 – 60 |
| `purposeCode` | enum | `INVENTORY` \| `WORKING_CAPITAL` \| `EXPANSION` \| `EQUIPMENT` |
| `selectedTier1Partner` | enum | `CredibleX` \| `CBI` \| `RAKBank` \| `Mbank` |
| `selectedTier2Partner` | string | Free-form Tier-2 partner id |
| `documentationCompleted` | object | Free-form per-document upload acknowledgement |
```bash theme={null}
curl -X PATCH https://apiv3.droplinked.com/lending-application/665fapp... \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"requestedAmountUsd": 50000,
"requestedTermMonths": 12,
"purposeCode": "INVENTORY",
"selectedTier1Partner": "CredibleX"
}'
```
### Submit / withdraw
```bash theme={null}
# Submit (DRAFT → SUBMITTED). 422-class error if required documents are missing.
curl -X POST https://apiv3.droplinked.com/lending-application/665fapp.../submit \
-H "Authorization: Bearer "
# Withdraw (→ WITHDRAWN). Optional reason.
curl -X POST https://apiv3.droplinked.com/lending-application/665fapp.../withdraw \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{ "reason": "No longer needed" }'
```
### Get document checklist
```
GET /lending-application/:id/checklist
```
Returns the per-cohort checklist (with the CredibleX add-on applied when relevant):
```json theme={null}
{
"cohort": "A_MOR_SHOPSADIQ_VIA_TELR",
"partner": "CredibleX",
"items": [
{
"key": "bank_statements_6m",
"label": "6 months of bank statements",
"description": "Statements covering the most recent 6 months from the bank account used for business deposits.",
"required": true,
"acceptedMimeTypes": ["application/pdf"],
"maxSizeMb": 25
},
{
"key": "tax_return_last",
"label": "Last tax return",
"description": "Most recent annual tax return for the business.",
"required": true,
"acceptedMimeTypes": ["application/pdf"],
"maxSizeMb": 20
},
{
"key": "adgm_stamped_certificate",
"label": "ADGM-stamped trade certificate",
"description": "Trade certificate carrying an ADGM apostille / stamp (UAE free-zone merchants).",
"required": true,
"acceptedMimeTypes": ["application/pdf", "image/png", "image/jpeg"],
"maxSizeMb": 10
}
]
}
```
Document completeness is validated against `documentationCompleted.items[]` (each item carries a
`key` + `status`; an item counts as uploaded when `status === "UPLOADED"`).
## Admin / lender routes (`/admin/lending-application`)
All routes require a JWT with the `SUPER_ADMIN` role.
| Operation | Method + path |
| ------------------- | -------------------------------------------------------------------------------------- |
| Review queue | `GET /admin/lending-application?status=&tier=&cohort=&partner=&limit=` |
| Get application | `GET /admin/lending-application/:id` |
| Post decision | `POST /admin/lending-application/:id/decision` |
| Credit-tier preview | `POST /admin/lending-application/credit-tier/preview` |
| Upgrade eligibility | `GET /admin/lending-application/credit-tier/upgrade-eligibility/:merchantId?baseTier=` |
### Review queue
Defaults to `status=UNDER_REVIEW`, sorted by `submittedAt` ascending. `limit` is clamped to
1–500 (default 100).
```bash theme={null}
curl "https://apiv3.droplinked.com/admin/lending-application?status=SUBMITTED&tier=TIER_1_REFERRAL&partner=CredibleX" \
-H "Authorization: Bearer "
```
### Post decision
```
POST /admin/lending-application/:id/decision
```
**Request**:
| Field | Required | Notes |
| -------------------- | -------- | ------------------------------------------------------------------- |
| `decision` | yes | `APPROVED` \| `REJECTED` \| `UNDER_REVIEW` |
| `reason` | no | Free-text reason |
| `partnerExternalRef` | no | The partner's external application reference (e.g. `cx_app_abc123`) |
```bash theme={null}
curl -X POST https://apiv3.droplinked.com/admin/lending-application/665fapp.../decision \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"decision": "APPROVED",
"reason": "Strong sell-through + onchain provenance coverage.",
"partnerExternalRef": "cx_app_abc123"
}'
```
### Credit-tier preview
A pure dry-run over `CreditTierMappingService` — no application lookup, no persistence.
```
POST /admin/lending-application/credit-tier/preview
```
**Request**:
| Field | Required | Notes |
| ----------------------- | -------- | -------------------------------------------------- |
| `cohort` | yes | A `KybCohort` value (drives the eligibility floor) |
| `monthlyAvgRevenueUsd` | yes | ≥ 0 |
| `verifiedCollateralUsd` | yes | ≥ 0 |
| `operationalRisk` | yes | `LOW` \| `MEDIUM` \| `HIGH` |
| `hardComplianceFlags` | no | e.g. `["OFAC_HIT"]` — hard flags force tier T0 |
```bash theme={null}
curl -X POST https://apiv3.droplinked.com/admin/lending-application/credit-tier/preview \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"cohort": "A_CONNECT_STRIPE",
"monthlyAvgRevenueUsd": 8000,
"verifiedCollateralUsd": 31777,
"operationalRisk": "LOW"
}'
```
Returns the resolved credit envelope (`tier`, `ceilingUsd`, `reason`).
## Errors
Standard NestJS error responses apply: `401` (missing/invalid JWT), `403` (missing
`SUPER_ADMIN` role on admin routes), `404` (`LENDING_PLATFORM_ENABLED` off, or application not
found), and `422`-class for an incomplete document checklist on submit.
The 12 cohorts that drive the per-application doc checklist.
# Lending Application API overview
Source: https://docs.droplinked.com/developers/lending-application-api/overview
Submit, update, and review merchant lending applications — state machine, per-cohort doc checklist, CredibleX referral model.
**BETA.** The Lending Application API powers the merchant-facing application UX and the operator/lender review surface. The whole surface is gated by the `LENDING_PLATFORM_ENABLED` server flag — every route returns `404` when it is off. CredibleX is the first Tier-1 referral partner (UAE corridor).
The **Lending Application API** wraps the merchant lending application lifecycle. There are two
surfaces:
* A **merchant-facing** surface at `/lending-application/*`, authenticated with the merchant's
JWT (the standard Droplinked auth token), ownership-checked at the service layer.
* An **operator/lender review** surface at `/admin/lending-application/*`, authenticated with a
JWT carrying the `SUPER_ADMIN` role.
There is no separate partner API key for lending today — partner referral flows (CredibleX)
operate against these surfaces with the appropriate credential.
## The state machine
```
┌───────────────┐
│ REJECTED │
└───────────────┘
▲
│
DRAFT ──submit──> SUBMITTED ──review──> UNDER_REVIEW ──decide──> APPROVED ──disburse──> DISBURSED
│ │
└──withdraw──> WITHDRAWN └──decide──> REJECTED
```
| State | Meaning | Who transitions |
| -------------- | --------------------------------------------- | -------------------------------------------------- |
| `DRAFT` | Application started; doc checklist incomplete | Merchant |
| `SUBMITTED` | Submitted, awaiting first review | Merchant (`POST :id/submit`) |
| `UNDER_REVIEW` | Operator/lender reviewing | Operator (`POST :id/decision` with `UNDER_REVIEW`) |
| `APPROVED` | Underwriter green-lit | Operator (`POST :id/decision` with `APPROVED`) |
| `REJECTED` | Application declined | Operator (`POST :id/decision` with `REJECTED`) |
| `WITHDRAWN` | Merchant withdrew | Merchant (`POST :id/withdraw`) |
| `DISBURSED` | Funds settled to merchant | System (post-disbursement) |
(Full enum: `DRAFT`, `SUBMITTED`, `UNDER_REVIEW`, `APPROVED`, `REJECTED`, `WITHDRAWN`,
`DISBURSED`.)
## KYB cohorts (11)
A merchant's application is keyed by their **KYB cohort**, which drives the document checklist and
lending-tier eligibility. The cohort enum (`KybCohort`) has 11 values:
`A_MOR_SHOPSADIQ_VIA_TELR`, `A_MOR_SHOPSADIQ_VIA_BONUM`, `A_MOR_TELR_DIRECT`,
`A_MOR_BONUM_DIRECT`, `A_CONNECT_TELR`, `A_CONNECT_BONUM`, `A_CONNECT_STRIPE`,
`A_CONNECT_PAYPAL`, `A_CONNECT_PAYMOB`, `B_SUMSUB_FALLBACK`, `C_MANUAL_OPERATOR`.
(The taxonomy is described in detail in [KYB cohorts](/developers/kyb-cohorts).)
## Per-cohort doc checklist
The checklist is derived from the merchant's cohort by `DocumentChecklistService`. It groups
into three burdens:
| Cohort family | Burden | Required documents (keys) |
| ----------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------- |
| `A_MOR_SHOPSADIQ_VIA_TELR`, `A_MOR_SHOPSADIQ_VIA_BONUM` | Light (3) | `bank_statements_6m`, `tax_return_last`, `adgm_stamped_certificate` |
| `A_MOR_TELR_DIRECT`, `A_MOR_BONUM_DIRECT` | Medium (4) | `bank_statements_6m`, `tax_return_last`, `trade_certificate`, `psp_settlement_statements_6m` |
| `A_CONNECT_TELR`, `A_CONNECT_BONUM`, `A_CONNECT_STRIPE`, `A_CONNECT_PAYPAL`, `A_CONNECT_PAYMOB` | Heavy (5) | `bank_statements_12m`, `tax_returns_2y`, `beneficial_ownership`, `trade_certificate`, `proof_of_address` |
| `B_SUMSUB_FALLBACK` | Fallback (4) | `bank_statements_12m`, `tax_returns_2y`, `trade_certificate`, `proof_of_address` |
| `C_MANUAL_OPERATOR` | Manual (3) | `bank_statements_6m`, `tax_return_last`, `trade_certificate` |
When the selected Tier-1 partner is **CredibleX**, an `adgm_stamped_certificate` item is
appended to the checklist if the base cohort didn't already require it.
**Rationale**: Shopsadiq-MoR cohorts inherit the Shopsadiq master KYB (light burden);
PSP-MoR-direct cohorts add PSP settlement statements (medium); Connect cohorts require the full
bank-and-tax pack because the merchant is the regulated entity (heavy).
## Lending tiers
Application tier (`LendingApplicationTier`): `TIER_1_REFERRAL`, `TIER_2_REFERRAL`,
`TIER_3_VAULT`. Tier-1 partners (`Tier1Partner` enum): `CredibleX`, `CBI`, `RAKBank`, `Mbank`.
## CredibleX referral model
CredibleX is the Tier-1 UAE-corridor lender. The referral flow:
1. **Submit / update** the application on the merchant's behalf via the merchant-facing surface.
2. **Read** application status + per-cohort checklist via `GET /lending-application/:id` and
`GET /lending-application/:id/checklist`.
3. **Decide** — an operator with `SUPER_ADMIN` posts `APPROVED` / `REJECTED` / `UNDER_REVIEW`
via `POST /admin/lending-application/:id/decision`.
## Auth
| Surface | Credential |
| ------------------------------ | --------------------------------------------------------------- |
| `/lending-application/*` | Merchant JWT (`Authorization: Bearer `); ownership-checked |
| `/admin/lending-application/*` | JWT with `SUPER_ADMIN` role |
Several merchant-facing write routes are additionally KYB-gated (`@KybGated()`) — the merchant
must have an approved KYB record to create/update/submit/withdraw.
## Base paths
```
https://apiv3.droplinked.com/lending-application
https://apiv3.droplinked.com/admin/lending-application
```
Create, patch, submit, decide — request / response shapes.
# Environments
Source: https://docs.droplinked.com/environments
Production and development base URLs for every Droplinked surface.
Droplinked runs three parallel environments (production, staging, development). Data is
**isolated per environment** — the same shop slug is different data in each. Build and
test against development, then promote.
## API & service base URLs
| Service | Production | Development |
| --------------------------- | -------------------------------- | ----------------------------------- |
| Core API (backend) | `https://apiv3.droplinked.com` | `https://apiv3dev.droplinked.com` |
| Integration Services (3rdp) | `https://service.droplinked.com` | `https://servicedev.droplinked.com` |
| Web3 Integration | `https://web3.droplinked.com` | `https://web3dev.droplinked.com` |
| Auth / OAuth | `https://auth.droplinked.com` | `https://authdev.droplinked.com` |
## Front-end surfaces
| Surface | Production | Development |
| ----------------------- | ------------------------------------------ | ----------------------------------- |
| Storefront | `https://droplinked.io` (+ custom domains) | `https://dev.droplinked.io` |
| Shop Builder (designer) | `https://droplinked.com` | `https://dev.droplinked.com` |
| Checkout | `https://checkout.droplinked.io` | `https://devcheckout.droplinked.io` |
## Health
```bash theme={null}
curl https://apiv3.droplinked.com/health
curl https://service.droplinked.com/health/stripe # per-integration health
```
The interactive Swagger UI is available on development at
`https://apiv3dev.droplinked.com/api-doc` (and the raw spec at `/api-doc-json`).
# Deploy flow
Source: https://docs.droplinked.com/guides/deploy-flow
How code reaches dev.droplinked.com and droplinked.com — the dev-first, manual-promote discipline that keeps production stable.
Droplinked ships every backend change through a **dev-first flow with a manual production
gate**. Dev (`dev.droplinked.com`) and live (`droplinked.com`) always run the same code from
the same branch — what diverges is *when* each environment redeploys.
## At a glance
```
PR merges to main
↓
Dev workflow auto-fires → dev.droplinked.com redeploys
↓
Smoke dev
↓
If green → operator clicks "Run workflow" on the main deploy
in the GitHub Actions UI → droplinked.com redeploys
↓
Re-smoke live immediately
↓
If regression → instant rollback to last-known-good ECS revision
```
Nothing reaches live without a green dev smoke first. No exceptions.
## Why this flow exists
The `droplinked-backend` repo drives two ECS services:
* A dev service backing `dev.droplinked.com`
* A live service backing `droplinked.com`
These deploy from the **same branch** (`main`) so dev can actually validate live behavior.
The dev workflow runs on every merge; the live workflow is `workflow_dispatch`-only — an
operator manually triggers it after the dev smoke is green.
GitHub Environment protection rules with required reviewers would be the prettier solution
but require a paid GitHub plan tier. The `workflow_dispatch`-only pattern is the no-cost
equivalent.
## How to deploy a change
Wait for CI (`verify`) to pass. Request a reviewer + merge.
`Deploy ECS - Dev` auto-fires on merge and completes in \~2–3 min.
Hit `dev.droplinked.com` — log in, exercise the affected endpoint, anything the diff
touches. Use a dev-only smoke merchant; never client accounts.
Go to **Actions → Deploy ECS - main → Run workflow** in the GitHub UI. Select branch
`main`, click **Run workflow**.
Smoke `droplinked.com` immediately after the deploy completes. If a regression appears,
revert the PR's commit or roll back the ECS service to the previous task-definition
revision before doing anything else.
## Emergency overrides
The `workflow_dispatch` trigger lets you select any branch when running a deploy manually
— useful for hotfixes that haven't merged yet.
**Emergency live deploy (critical patch, can't wait for the standard flow):** Run
`Deploy ECS - main` against the branch carrying the fix. Skip the dev smoke step but
document the override in the post-incident notes.
**Emergency dev deploy of a non-`main` branch (testing an unmerged fix):** Run
`Deploy ECS - Dev` against the branch under test.
## Related
* See [Environment variables](/guides/environment-variables) for the runtime configuration
surface each environment expects.
* See [Order lifecycle](/guides/order-lifecycle) for what an end-to-end smoke should
exercise post-deploy.
# Environment variables
Source: https://docs.droplinked.com/guides/environment-variables
Reference for runtime configuration of the Droplinked backend — core app, payments, email, and affiliate network.
All secrets live in a secrets manager. Real values are never committed to source — `.env`
files in the repo contain placeholders only.
Never put live keys (Stripe, PayPal, AWS, Atlas) in plaintext config. The team configures
production values through AWS Secrets Manager and they're loaded into the ECS task at boot.
## Core application
| Variable | Type | Description | Example |
| -------------- | ------ | ---------------------------------------------------------- | ------------------- |
| `APP_PORT` | number | HTTP port the server listens on | `80` |
| `NODE_ENV` | string | Runtime environment | `production` |
| `DATABASE_URL` | string | MongoDB connection string (Prisma) | `mongodb+srv://...` |
| `REDIS_URL` | string | Redis connection string (managed — ElastiCache or Upstash) | `redis://...` |
| `JWT_SECRET` | string | Secret for signing JWTs | — |
## Stripe
| Variable | Type | Description | Example |
| ----------------------- | ------ | ----------------------------- | ------------- |
| `STRIPE_SECRET_KEY` | string | Stripe secret key | `sk_live_...` |
| `STRIPE_WEBHOOK_SECRET` | string | Stripe webhook signing secret | `whsec_...` |
## PayPal
| Variable | Type | Description | Example |
| ---------------------- | ------ | ---------------------------------------------------- | ------- |
| `PAYPAL_MODE` | string | `sandbox` or `live` | `live` |
| `PAYPAL_CLIENT_ID` | string | PayPal OAuth2 client ID | — |
| `PAYPAL_CLIENT_SECRET` | string | PayPal OAuth2 client secret (a.k.a. `PAYPAL_SECRET`) | — |
| `PAYPAL_PARTNER_ID` | string | PayPal Partner ID for marketplace onboarding | — |
| `PAYPAL_BN_CODE` | string | Build Notation Code (attribution) | — |
| `PAYPAL_WEBHOOK_ID` | string | ID of the webhook listener configured in PayPal | — |
## Telr
| Variable | Type | Description | Example |
| ---------------- | ------- | ------------------------------------- | ------- |
| `TELR_STORE_ID` | string | Telr store ID issued by Telr | — |
| `TELR_AUTH_KEY` | string | Telr auth key (rotate quarterly) | — |
| `TELR_BASE_URL` | string | Defaults to `https://secure.telr.com` | — |
| `TELR_ENABLED` | boolean | Master gate for Telr routing | `true` |
| `TELR_TEST_MODE` | boolean | Send `ivp_test=1` on order create | `false` |
## Bonum
| Variable | Default | Notes |
| -------------------------- | ----------- | -------------------------------------- |
| `BONUM_API_BASE_URL` | — | `https://testpsp.bonum.mn` for sandbox |
| `BONUM_MERCHANT_KEY` | — | Issued by MCredit |
| `SETTLEMENT_CRON_SCHEDULE` | `0 2 * * *` | Adjust for the operating timezone |
| `MERCHANT_PREFIX_DIGITS` | `6` | Confirm with the PSP before go-live |
## Coinbase Commerce
| Variable | Type | Description |
| ---------------------------------- | ------ | --------------------------------------- |
| `COINBASE_COMMERCE_API_KEY` | string | Coinbase Commerce API key |
| `COINBASE_COMMERCE_WEBHOOK_SECRET` | string | Coinbase Commerce webhook shared secret |
## Fulfillment
| Variable | Type | Description |
| ------------------- | ------ | -------------------------------------------- |
| `EASY_POST_API_KEY` | string | Private API key from your EasyPost dashboard |
| `PRINTFUL_API_KEY` | string | API key from Printful → Settings → API |
| `PRINTFUL_STORE_ID` | string | Store ID from Printful's stores section |
## Email
| Variable | Type | Description | Example |
| ------------------ | ------ | ---------------------------------------- | ------------------------ |
| `SENDGRID_API_KEY` | string | SendGrid API key for transactional email | — |
| `EMAIL_FROM` | string | Sender address | `noreply@droplinked.com` |
## Affiliate network
| Variable | Type | Description |
| -------------------------------------- | ------ | ------------------------------------------------------------------------------- |
| `AFFILIATE_KMS_KEY_ARN` | string | ARN of the KMS `ECC_SECG_P256K1` key used to sign USDC payout transactions |
| `AFFILIATE_REDIRECT_BASE_URL` | string | Public base URL for affiliate redirect links (e.g. `https://go.droplinked.com`) |
| `AFFILIATE_COOKIE_SECRET` | string | HMAC secret for signing the `dl_aff` attribution cookie |
| `AFFILIATE_COMMISSION_DEFAULT_PERCENT` | number | Default commission rate when not set per product |
| `AFFILIATE_ATTRIBUTION_TTL_DAYS` | number | Days before an attribution session expires |
| `USDC_PAYOUT_WALLET_ADDRESS` | string | Avalanche C-Chain wallet address for USDC payouts (must be funded) |
| `AVALANCHE_RPC_URL` | string | Avalanche C-Chain RPC endpoint |
Rotate `AFFILIATE_COOKIE_SECRET` and `JWT_SECRET` every 90 days. KMS keys are rotated on a
coordinated schedule with the security lead.
## Related
* [Deploy flow](/guides/deploy-flow) — how config changes reach dev and live.
* [Supply chain integrity](/guides/security/supply-chain) — how the build verifies what
gets shipped alongside these values.
# Bonum
Source: https://docs.droplinked.com/guides/integrations/bonum
Bonum PSP integration — Mongolia-region settlement (MNT), Apple/Google Pay tokens, off-platform payout reconciliation.
Bonum is a payment service provider operating in Mongolia. The Droplinked integration creates
invoices via the Bonum API, accepts Apple/Google Pay tokens through Bonum's hosted PSP, and
reconciles settlement back into Droplinked's revenue-distribution saga.
Bonum settles **off-platform**: the saga records the revenue-split plan, then a downstream
operator (typically MCredit) pays sub-merchants and reports back via the settlement API.
## Architecture
* **Three Prisma models:** `BonumTransaction`, `MerchantPrefixRegistry`, `BonumConfig`
* **NestJS module:** dedicated `bonum` module on the backend
* **Payment strategy:** `BonumPaymentStrategy` — registered in `PaymentFactory` under `provider === 'BONUM'`
* **Saga step:** the revenue-distribution step forks for Bonum orders (records splits, no live transfer)
* **Webhook receiver:** `POST /bonum/webhook` (`IntegrationApiKeyGuard`)
* **Settlement cron:** two daily audit passes (`SettlementReconciliationCron`)
## API surface
| Route | Purpose |
| --------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `POST /orders/v2/create-payment-intent` (with `provider=BONUM`) | Creates `BonumTransaction`, returns invoice ID + Apple/Google Pay token |
| `POST /bonum/payment/confirm` | Confirms payment via the integration service → Bonum PSP |
| `POST /bonum/webhook` | Settlement event from Bonum; signature-verified, idempotent |
| `GET /settlement/summary` | Settlements grouped by merchant prefix |
| `GET /settlement/transactions` | Settlement-transaction listing |
| `PATCH /settlement/transactions/payout` | Mark transactions as paid out to sub-merchants |
| `GET /merchants/:shopId/balance` | Pending payable balance for a shop |
## Configuration
| Variable | Default | Notes |
| -------------------------- | ----------- | -------------------------------------------------------------------- |
| `BONUM_API_BASE_URL` | — | `https://testpsp.bonum.mn` for sandbox |
| `BONUM_MERCHANT_KEY` | — | Issued by the Bonum / MCredit team |
| `SETTLEMENT_CRON_SCHEDULE` | `0 2 * * *` | Adjust for the operating timezone (Ulaanbaatar UTC+8 = `0 18 * * *`) |
| `MERCHANT_PREFIX_DIGITS` | `6` | Confirm with MCredit before go-live |
## Payment flow
`POST /orders/v2/create-payment-intent` with `provider=BONUM`. The `PaymentFactory`
routes to `BonumPaymentStrategy.createPaymentIntent()`, which upserts a
`BonumTransaction` and returns an invoice ID prefixed with the shop's merchant prefix.
The frontend uses the returned Apple/Google Pay token via the Bonum hosted page.
`POST /bonum/payment/confirm` calls the integration service → Bonum PSP. The verify
use case then confirms via `/api/payment-log/read`.
The standard `ConfirmPaymentSaga` runs; for Bonum the revenue-distribution step
**records** splits rather than executing live transfers.
Bonum POSTs to `/bonum/webhook` → `HandleBonumWebhookUseCase` sets `settledAt`.
MCredit (or the equivalent operator) reads the settlement API, pays sub-merchants, then
calls `PATCH /settlement/transactions/payout` to mark the batch paid.
## Testing
### Sandbox setup
The Bonum sandbox at `testpsp.bonum.mn` **issues real bank charges**. Use minimum amounts
(100 MNT) for every test.
1. Set `BONUM_API_BASE_URL=https://testpsp.bonum.mn`
2. Obtain a sandbox `BONUM_MERCHANT_KEY` from the MCredit team
3. Confirm `NODE_ENV` is **not** `production`
### Unit tests
```bash droplinked-backend theme={null}
npx jest --testPathPattern=bonum --no-coverage
```
```bash 3rdp-integration-services theme={null}
npx jest --testPathPattern=bonum --no-coverage
```
### Critical scenarios
| Scenario | Expected |
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Shop with `ACTIVE` prefix creates intent | 24-char alphanumeric invoice ID |
| Shop with no prefix | `BadRequestException` |
| Shop with `SUSPENDED` prefix | `BadRequestException` |
| Two `createOrGet` calls with same invoice ID | Second returns existing record (no duplicate) |
| Revenue splits | `droplinkedFeeAmount + merchantPayable + affiliateAmount + referralAmount === amountMnt`; live transfer **not** called |
| Webhook delivered twice with same `_eventId` | First sets `settledAt`; second is silently discarded |
| Invalid payout status transition (e.g. skipping `PROCESSING` → `PAID`) | 400 with "Invalid payout status transition" |
### Settlement API examples
```bash theme={null}
# Group settlements by merchant prefix over a date range
GET /settlement/summary?date_from=2026-03-01&date_to=2026-03-19
# Mark transactions as processing payout
PATCH /settlement/transactions/payout
{
"invoiceIds": ["..."],
"payoutStatus": "PROCESSING",
"payoutReference": null
}
# Look up a shop's pending balance
GET /merchants/:shopId/balance
```
### Cron audit
The settlement-reconciliation cron runs two daily passes:
* **Pass 1** — warns on `BonumTransaction` rows that are verified but unsettled after 48h
* **Pass 2** — warns on rows that remain unverified after 24h
### E2E against sandbox
Requires a sandbox `BONUM_MERCHANT_KEY` and an active `MerchantPrefixRegistry` entry.
Create a shop with `bonumEnabled=true` and assign a merchant prefix.
`POST /orders/v2/create-payment-intent` with `provider=BONUM` → confirm the returned
invoice ID starts with the shop's prefix.
Use a real Apple/Google Pay token from `testpsp.bonum.mn`, POST it with the invoice ID
to the integration service's `/bonum/payment/process` endpoint.
`GET /bonum/payment-log/read?invoiceId=` should return `success=true`.
## Related
* [Order lifecycle](/guides/order-lifecycle) — how Bonum slots into the confirmation saga.
* [Checkout stability](/guides/testing/checkout-stability) — the broader test matrix.
# EasyPost
Source: https://docs.droplinked.com/guides/integrations/easypost
EasyPost integration — multi-carrier shipping. Address verification, parcel creation, rate retrieval, label purchase, and tracking.
EasyPost is a shipping API that abstracts the complexity of multiple carriers (USPS, UPS,
FedEx, and more) behind a single integration. Droplinked uses it within the
`ShippingModule` for:
* Address verification
* Parcel creation
* Shipment creation and rate retrieval
* Purchasing shipping labels
* Tracking shipments
## Configuration
```env theme={null}
EASY_POST_API_KEY=...
```
| Var | Notes |
| ------------------- | ------------------------------------------------------------------------- |
| `EASY_POST_API_KEY` | Private API key from your EasyPost dashboard. Authenticates every request |
`EASY_POST_API_KEY` is sensitive and stored server-side only. Never expose it to the
client.
## Client initialization
The `EasyPostShippingService` initializes the client in its constructor using the
`@easypost/api` library:
```ts theme={null}
this.easyPost = new EasyPostClient(apiKey, { timeout: 120000 });
```
## Reference docs
* [EasyPost API Documentation](https://docs.easypost.com/)
## API surface
### Address creation
Fields sent:
* `street1`, `street2`
* `city`, `state`, `zip`
* `country`
* `company`, `phone`, `email`
### Shipment creation
Fields sent:
* `to_address` — destination address ID
* `from_address` — origin address ID
* `parcel` — parcel ID
* `customs_info` — required for international shipments (contents type, etc.)
Returned:
* `id` — shipment ID
* `rates[]` — array of available rates, each with `id`, `carrier`, `service`, `rate`,
`delivery_days`
### Buying a shipment
Fields sent:
* `shipment_id` — the shipment to buy
* `rate` — `{ id: rate_id }`, the specific rate selected
Returned:
* `tracking_code` — tracking number
* `postage_label` — URL to the shipping label image (PNG/PDF)
* `status` — current status (`pre_transit`, `in_transit`, `delivered`, …)
## What we store
| Field | Why |
| --------------------- | --------------------------------- |
| `shipment_id` | Reference the shipment later |
| `tracking_code` | Let customers track their package |
| `label_url` | Re-print the label on demand |
| `carrier` & `service` | Record-keeping and analytics |
## Flow diagram
```mermaid theme={null}
sequenceDiagram
participant User
participant Backend
participant EasyPost
User->>Backend: Request shipping rates
Backend->>EasyPost: Create address (from + to)
EasyPost-->>Backend: Address IDs (verified)
Backend->>EasyPost: Create parcel
EasyPost-->>Backend: Parcel ID
Backend->>EasyPost: Create shipment
EasyPost-->>Backend: Shipment ID + rates
Backend-->>User: Return rates
User->>Backend: Buy shipping (shipmentId, rateId)
Backend->>EasyPost: Buy shipment
EasyPost-->>Backend: Label URL + tracking code
Backend-->>User: Purchase confirmation
```
## Security notes
`EASY_POST_API_KEY` is server-only and stored in your secrets manager. Never inlined in
client code or build output.
EasyPost address verification runs **before** buying labels — reduces failed
deliveries.
## Troubleshooting
| Symptom | Likely cause | Fix |
| ------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------- |
| `Service unavailable` | EasyPost API down or timing out | Client uses a 120s timeout; retry the request |
| `Rate not found` | `rate_id` invalid or expired | Refresh rates by creating a new shipment and selecting a fresh rate |
| Empty `availableShipping` on a US→US physical-only cart | Product mis-routed to a non-EasyPost strategy | See [Checkout stability — known regression](/guides/testing/checkout-stability) |
## Related
* [Order lifecycle](/guides/order-lifecycle)
* [Printful](/guides/integrations/printful) — the parallel path for POD goods.
* [Checkout stability](/guides/testing/checkout-stability) — shipping test matrix.
# Embed Trust-Fabric Widget
Source: https://docs.droplinked.com/guides/integrations/embed-trust-fabric-widget
Surface your droplinked trust-fabric posture (brand attestation, credit line, lender associations) on your own merchant dashboard or storefront with copy-paste React + vanilla JS snippets.
Most merchants finish onboarding, get their brand attestation hash, get matched
with a lender, and then never see any of it again. The trust-fabric posture they
spent the onboarding flow earning sits in the SUPER\_ADMIN console or in an
attestation API they don't know they can query. This page closes that loop: a
drop-in widget that surfaces your trust-fabric posture — brand attestation
status, credit line, recent lender associations, repayment history — on your
own merchant dashboard, custom storefront, or external admin portal.
Every endpoint the widget polls is `@Public()` — no JWT, no SDK, no IP
allowlist. Copy the snippet, swap the two placeholder constants for your
shop slug and merchant ID, and the widget renders.
## What the widget shows
* Your **Schema A brand attestation** status — `ACTIVE`, `PENDING`, or
not-yet-issued.
* Your **associated lender(s)** sourced from the `LenderRegistry` via the
routing recommender.
* Your most-recent **credit-risk attestation (Schema B)**, if any — credit
line amount + issuing lender.
* Your **repayment-history (Schema C)** rollup — successful settlements count.
* Your **trust-tier** (if `/v2/upgrade-preview` returns one — `starter`,
`verified`, `trusted`, etc.).
* Aggregate **trust-fabric scale** from `/v2/trust-fabric/stats` so you can
display a "Powered by Droplinked Trust Fabric" badge with live platform
numbers.
## Live data sources
| Endpoint | What it surfaces |
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| [`GET /v2/attestations/brand/:slug`](/concepts/trust-fabric) | Schema A brand attestation status |
| [`GET /v2/attestations/credit-risk/:merchantId`](/concepts/trust-fabric) | Schema B credit-risk attestation (current credit line + issuing lender) |
| [`GET /v2/attestations/repayment-history/:merchantId`](/concepts/trust-fabric) | Schema C repayment rollup |
| [`GET /v2/upgrade-preview`](/api-reference/public/upgrade-preview) | Trust-tier projection |
| [`GET /v2/lender-routing/recommend?jurisdiction=...`](/api-reference/public/lender-routing) | Associated / recommended lenders |
| [`GET /v2/trust-fabric/stats`](/api-reference/public/trust-fabric-stats) | Aggregate platform badge numbers |
All six endpoints are public + unauthenticated. The widget calls them in
parallel and renders whatever returns; missing endpoints (e.g. no credit-risk
attestation yet) degrade gracefully to "none active".
## Widget code
Self-contained — drop into any existing HTML page or merchant admin
template.
```html theme={null}
```
Drop the hook + component into any existing React app. Uses plain
`useEffect` polling — swap for SWR or React Query if you've already
standardized on one.
```tsx theme={null}
import { useEffect, useState } from 'react';
const API = 'https://apiv3.droplinked.com';
type TrustFabricData = {
brand: { status?: string } | null;
credit: { attestation?: { creditLineCents?: number; lenderId?: string } } | null;
repayment: { summary?: { successfulSettlements?: number } } | null;
upgrade: { currentTier?: string } | null;
stats: {
lenders?: { active?: number };
attestations?: { schemaB?: number };
} | null;
};
function useTrustFabric(shopSlug: string, merchantId: string) {
const [data, setData] = useState(null);
useEffect(() => {
let cancelled = false;
async function load() {
const [brand, credit, repayment, upgrade, stats] = await Promise.all([
fetch(`${API}/v2/attestations/brand/${shopSlug}`).then((r) => r.json()).catch(() => null),
fetch(`${API}/v2/attestations/credit-risk/${merchantId}`).then((r) => r.json()).catch(() => null),
fetch(`${API}/v2/attestations/repayment-history/${merchantId}`).then((r) => r.json()).catch(() => null),
fetch(`${API}/v2/upgrade-preview?merchantId=${merchantId}`).then((r) => r.json()).catch(() => null),
fetch(`${API}/v2/trust-fabric/stats`).then((r) => r.json()).catch(() => null),
]);
if (!cancelled) setData({ brand, credit, repayment, upgrade, stats });
}
load();
const id = setInterval(load, 5 * 60_000);
return () => {
cancelled = true;
clearInterval(id);
};
}, [shopSlug, merchantId]);
return data;
}
export function TrustFabricWidget({
shopSlug,
merchantId,
}: {
shopSlug: string;
merchantId: string;
}) {
const data = useTrustFabric(shopSlug, merchantId);
if (!data) return
Powered by Droplinked Trust Fabric — {activeLenders} active lenders,{' '}
{schemaB} on-chain credit attestations
);
}
```
## Privacy + auth
Every endpoint this widget calls is decorated with `@Public()` in the API —
no JWT, no API key, no IP allowlist. That means you can embed the snippet
directly in client-side code on a shop builder template or a custom
storefront without proxying through a backend.
The fields surfaced are public-safe by design:
* Statuses, tier labels, credit line amount, settlement counts.
* No operator-side actor IDs.
* No signing wallet addresses.
* No regulator references or audit reasons.
* No counterparty PII.
Treat this widget as the merchant-facing analog of the public lender
registry — everything is meant to be visible, and nothing leaks the
operator-only fields that live behind SUPER\_ADMIN.
## Customization
* **Brand chrome** — swap the `#2bcfa1` / `#06c295` / `#11221c` palette for
your own; replace the `
` heading with your own copy or logo.
* **Hide cards** — remove the credit-line card on a storefront where you
only want to show brand attestation, or remove the "Powered by" footer on
an internal admin portal.
* **Per-tier badges** — render a different SVG / color for each
`upgrade.currentTier` value (`starter` / `verified` / `trusted`).
* **Lender association list** — call
`/v2/lender-routing/recommend?jurisdiction=YOUR_GEO` and render the top-N
recommended lenders alongside the credit line for a "who'd underwrite you
today" view.
* **Link to forensic chain** — if you also want to expose attestation hashes,
link `brand.attestationUid` through to the public block explorer rather
than rendering the hex on the card itself.
## Polling cadence
Recommend a **client-side refresh of 5 minutes or greater** for this widget.
The endpoints used here are aggregate / lookup endpoints — they don't change
at high velocity. The reference [Trust-Fabric Dashboard Template](/guides/integrations/trust-fabric-dashboard-template)
uses 60-second polling because it's a platform-aggregate dashboard; a
per-merchant posture widget changes more slowly (status flips, periodic
credit-line refreshes), so 5 minutes is plenty.
If you embed multiple instances of this widget on the same page, cache the
fetch responses at module scope or behind SWR / React Query with
`staleTime: 300_000` to avoid duplicate requests.
## CORS
All endpoints used here are CORS-permissive — they respond with
`Access-Control-Allow-Origin: *` to browser fetches from any origin. You
can verify locally:
```bash theme={null}
curl -sI -H "Origin: https://yourdomain.example" \
https://apiv3.droplinked.com/v2/trust-fabric/stats | grep -i access-control
```
If a specific origin fails, email `support@droplinked.com` with the origin
* the failing request — it usually indicates a WAF rule rather than a CORS
policy.
## What this widget does NOT show
Operator-only fields stay in the SUPER\_ADMIN console and never appear in
the public attestation endpoints — so they don't appear in this widget
either:
* Signing wallet addresses + key custody references.
* Per-attestation audit-log entries (who issued, when, on what host).
* Operator notes / underwriting reasons.
* Regulator correspondence references.
* Lender-side risk scoring detail.
If you need any of those surfaces for an internal admin tool, build it
behind the SUPER\_ADMIN JWT — don't try to extend this widget.
## Related
* [Trust-Fabric Dashboard Template](/guides/integrations/trust-fabric-dashboard-template) — platform-aggregate dashboard (for lenders / partner portals)
* [For Merchants](/concepts/for-merchants) — merchant-side narrative for the trust-fabric value prop
* [Trust Fabric (EAS Schema v2)](/concepts/trust-fabric) — 4-axis architecture overview
* [Trust-Fabric Stats](/api-reference/public/trust-fabric-stats) — aggregate endpoint reference
* [Upgrade Preview](/api-reference/public/upgrade-preview) — trust-tier projection endpoint
* [Lender Routing](/api-reference/public/lender-routing) — jurisdiction-ranked lender recommender
# PayPal
Source: https://docs.droplinked.com/guides/integrations/paypal
PayPal integration — merchant onboarding via Partner Referral, end-customer payments via the PayPal SDK, and signed webhook handling.
PayPal is implemented in two parts:
1. **Merchant onboarding** — registering and connecting seller accounts as Partners
2. **Payment gateway** — processing end-customer payments, creating orders, and managing
webhooks
These services let the platform authenticate sellers and manage financial transactions.
## Configuration
```env theme={null}
# General
PAYPAL_MODE=sandbox # 'sandbox' or 'live'
# API credentials
PAYPAL_CLIENT_ID=... # from PayPal Developer Dashboard
PAYPAL_SECRET=... # client secret (a.k.a. PAYPAL_CLIENT_SECRET)
# Partner / onboarding
PAYPAL_PARTNER_ID=... # your PayPal Partner ID
PAYPAL_BN_CODE=... # Build Notation Code (attribution)
# Webhook
PAYPAL_WEBHOOK_ID=... # ID of the listener configured in PayPal
```
| Var | Notes |
| ------------------------------------ | ----------------------------------------------------------- |
| `PAYPAL_MODE` | Determines the execution environment |
| `PAYPAL_CLIENT_ID` / `PAYPAL_SECRET` | Main API keys — must be kept secret |
| `PAYPAL_PARTNER_ID` | Used for Marketplace features and seller referrals |
| `PAYPAL_WEBHOOK_ID` | Essential for validating the signature of incoming webhooks |
## Two clients
| Surface | Library | Purpose |
| ------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------- |
| **Onboarding** | Raw HTTP (`fetch`) | Token + Partner Referral management. Access tokens fetched dynamically per request |
| **Payment Gateway** | `@paypal/checkout-server-sdk` | Creating orders and financial transactions. SDK client configured for `SandboxEnvironment` or `LiveEnvironment` |
## Merchant onboarding
### 1. Create Partner Referral
* **Flow:** User → Core Backend → Integration Services → PayPal API
* **Internal endpoint:** `POST /paypal/partner-referrals`
* **PayPal endpoint:** `POST /v2/customer/partner-referrals`
```json theme={null}
{
"tracking_id": "merchant-695d341eb52f8c355adf558d",
"partner_config_override": {
"return_url": "https://dev.droplinked.com/analytics/account-settings",
"return_url_description": "Return after onboarding"
},
"operations": [
{
"operation": "API_INTEGRATION",
"api_integration_preference": {
"rest_api_integration": {
"integration_method": "PAYPAL",
"integration_type": "THIRD_PARTY"
}
}
}
],
"products": ["EXPRESS_CHECKOUT"],
"legal_consents": [
{ "type": "SHARE_DATA_CONSENT", "granted": true }
]
}
```
**Response** includes an `onboarding_url` to redirect the seller to.
### 2. Onboarding return (callback)
* **Flow:** PayPal → User redirect → Core Backend
* The Core Backend receives `merchantId` and `trackingId` in the URL and persists the
`merchantId`.
This step does **not** call the Integration Service.
### 3. Verify merchant integration
* **Internal endpoint:** `GET /paypal/merchant-integrations/:merchantId`
Checks whether the merchant is fully authorized to receive payments.
```json theme={null}
{
"statusCode": 200,
"status": "success",
"data": {
"payments_receivable": true,
"capabilities": ["PAYPAL_CHECKOUT", "GUEST_CHECKOUT", "SEND_INVOICE"],
"vetting_status": "SUBSCRIBED"
}
}
```
If `payments_receivable === true` and `vetting_status !== 'DENIED'`, the Core Backend
enables the PayPal gateway for that shop.
## Payment & checkout
### Create payment intent
* **Internal endpoint:** `POST /payment-gateway/create-intent`
**Scenario A — merchant account connected.** Funds transfer directly or split:
```json theme={null}
{
"type": "paypal",
"amount": 100,
"currency": "USD",
"description": "DROPLINKED Paypal Payment",
"platform_fee_amount": 1,
"metadata": {
"orderId": "6981d32bcbfd8f75959dad9e",
"intent": "CAPTURE",
"return_url": "...",
"cancel_url": "...",
"merchantAccountId": "TXFGGV8K3267A",
"transfer_to_paypal_account": 1
}
}
```
**Scenario B — merchant account not connected.** Funds stay in the platform's primary
PayPal account:
```json theme={null}
{
"type": "paypal",
"amount": 10,
"currency": "USD",
"description": "DROPLINKED Paypal Payment",
"platform_fee_amount": 0.2,
"metadata": {
"orderId": "6981d82dcbfd8f75959dada9",
"intent": "CAPTURE",
"return_url": "...",
"cancel_url": "...",
"merchantAccountId": null,
"transfer_to_paypal_account": 0
}
}
```
### Webhook handling & normalization
* **Flow:** PayPal → Integration Services → Core Backend (`/webhook/generic`)
When the Integration Service receives a PayPal webhook, it validates the signature and
sends a normalized payload to the Core Backend:
```json theme={null}
{
"event": {
"status": "COMPLETED",
"orderId": "6981d32bcbfd8f75959dad9e",
"transactionId": "PAYPAL-TX-ID",
"transactionLink": "https://paypal.com/activity/payment/..."
},
"type": "paypal"
}
```
### Status mapping
| PayPal status | Core Backend action | Result |
| ---------------------- | ------------------- | ----------------------------------------------- |
| `COMPLETED` | `confirmPayment()` | Order confirmed, inventory updated, emails sent |
| `REFUNDED` | Update status | Order status → `REFUNDED` |
| `PENDING` | Update status | Payment status → `PENDING` |
| `FAILED` / `DENIED` | Update status | Payment status → `FAILED` |
| `CANCELLED` / `VOIDED` | Update status | Order status → `CANCELLED` |
## Flow diagrams
### Merchant onboarding
```mermaid theme={null}
sequenceDiagram
participant Merchant
participant Core Backend
participant Integration Services
participant PayPal API
Merchant->>Core Backend: Request Onboarding Link (POST /onboarding-link)
Core Backend->>Integration Services: Create Partner Referral
Integration Services->>PayPal API: POST /v2/customer/partner-referrals
PayPal API-->>Integration Services: Return onboarding_url
Integration Services-->>Core Backend: Return onboarding_url
Core Backend-->>Merchant: Redirect to onboarding_url
Merchant->>PayPal API: Complete setup & grant permissions
PayPal API-->>Merchant: Redirect to return_url
Merchant->>Core Backend: GET /return (with merchantId)
Core Backend->>Core Backend: Save merchantId
Core Backend->>Integration Services: Verify status (GET /paypal/merchant-integrations/:id)
```
### Payment checkout
```mermaid theme={null}
sequenceDiagram
participant Customer
participant Core Backend
participant Integration Services
participant PayPal API
Customer->>Core Backend: Create intent (POST /orders/:id/payment-intent)
Core Backend->>Integration Services: Create intent
Integration Services->>PayPal API: Create order (SDK)
PayPal API-->>Integration Services: Order ID + approve URL
Integration Services-->>Core Backend: Normalized order data
Core Backend-->>Customer: checkout_url
Customer->>PayPal API: Approve payment
PayPal API-->>Integration Services: Webhook (PAYMENT.CAPTURE.COMPLETED)
Integration Services->>Integration Services: Verify webhook signature
Integration Services->>Core Backend: POST /webhook/generic (normalized event)
Core Backend->>Core Backend: confirmPayment()
```
## Security notes
All webhooks are validated using `paypal-transmission-sig` and `PAYPAL_WEBHOOK_ID`. The
signature includes `transmissionId`, `timestamp`, `webhookId`, and the CRC32 of the
request body.
A new access token is obtained for each onboarding request (Client Credentials Flow).
No sensitive tokens are logged.
`crypto.timingSafeEqual` (or `safeCompareHmac`) is used to compare signatures and
prevent timing attacks.
## Troubleshooting
| Symptom | Likely cause |
| ----------------------- | ---------------------------------------------------------------------- |
| All webhooks rejected | `PAYPAL_WEBHOOK_ID` in env doesn't match the PayPal Dashboard settings |
| Currency mismatch error | `currency_code` doesn't match the seller's PayPal account settings |
| `401 Unauthorized` | Using sandbox keys in live (or vice versa) |
## Related
* [Order lifecycle](/guides/order-lifecycle)
* [Checkout stability](/guides/testing/checkout-stability)
# Printful
Source: https://docs.droplinked.com/guides/integrations/printful
Printful integration — print-on-demand catalog, embedded design maker, mockup generation, shipping rates, and order creation.
Printful is Droplinked's print-on-demand production and fulfillment service. The integration
covers catalog browsing, embedded design workflows, mockup generation, shipping rate
calculation, and order creation.
This integration powers the **POD** (Print-on-Demand) subsystem.
## Configuration
```env theme={null}
PRINTFUL_API_KEY=...
PRINTFUL_STORE_ID=...
```
| Var | Where to get it |
| ------------------- | ----------------------------------------- |
| `PRINTFUL_API_KEY` | Printful admin dashboard → Settings → API |
| `PRINTFUL_STORE_ID` | Printful stores section |
These values are loaded server-side only. Never expose them to the frontend.
## Client initialization
The Printful client is initialized with `PRINTFUL_API_KEY` attached as a Bearer token,
shared across all Printful service modules.
## Reference docs
* [Printful general docs](https://developers.printful.com/docs/)
* [V2 Beta](https://developers.printful.com/docs/v2-beta/)
* [Embedded Designer (EDM)](https://developers.printful.com/docs/edm/)
## API surface
### Catalog
| Internal | Printful | Notes |
| --------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------- |
| `GET /printful/categories` | `GET /categories` | id, parent\_id, image\_url, catalog\_id, title |
| `GET /printful/products?categoryId=...` | `GET /categories/{id}` | id, variants, title, brand |
| `GET /pod/product/{id}` | `GET /products/{id}` | product, variants, files, options, techniques |
| `GET /printful/shipping/{id}` | `GET /v2/catalog-products/{id}/availability?selling_region_name=all` | availability, regions, warehouse\_locations |
### Designer
| Internal | Printful | Notes |
| ----------------------------------------------------------------- | -------------------------------- | --------------------------------------------------------------------------- |
| `POST /printful/nonce` | `POST /embedded-designer/nonces` | Returns `nonce` + `expires_at`. Outgoing: `template_id` (optional), `scope` |
| `GET /pod/available-variants/{provider}/{productId}/{templateId}` | `GET /products/{id}` | Used after the designer creates a template |
### Mockups
| Internal | Printful |
| -------------------------------- | ----------------------------------------- |
| `POST /printful/generate/mockup` | `POST /mockup-generator/create-task/{id}` |
Returns `task_key`, `result_url`, `status`.
### Orders
* **Printful endpoint:** `POST /orders`
* **Sent:** `recipient`, `items[]`, `variant_id`, `files[]` (artwork URLs, positions,
template references), `external_id`, `shipping`, `store_id`
### Shipping rates
* **Printful endpoint:** `POST /shipping/rates`
* **Sent:** `recipient.address`, `items[].quantity`, `items[].variant_id`
* **Returned:** `rate`, `carrier`, `service`, `minDeliveryDays` / `maxDeliveryDays`
## What we store (POD object)
```ts theme={null}
type ProductPodV2 = {
artwork: string | null
artwork2: string | null
artwork_position: string | null
artwork2_position: string | null
printful_template_id: number | null
pod_blank_product_id: number | null
prodviderID: string | null
m2m_positions_options: ProductsM2MPositions[]
m2m_positions: string[]
m2m_services: string[]
custome_external_id: string | null
printful_option_data: PrintfulOptionData[]
positions: ProductPodPositions | null
technique: string | null
}
```
**Why we store it:**
* `artwork` / artwork positions — needed during Printful file upload at order creation
* `printful_template_id` — required to retrieve variant-specific print files
* `pod_blank_product_id` — core product ID for Printful orders
* `option/position` data — required for mockup generation + order fulfillment
* `technique` — printing method (DTG, sublimation, embroidery, etc.)
## Flow diagram
```mermaid theme={null}
sequenceDiagram
participant Frontend
participant Backend
participant Printful
participant DB
Frontend->>Backend: Request categories/products/details
Backend->>Printful: Catalog API
Printful-->>Backend: Category/product data
Backend->>DB: Cache product metadata (optional)
Frontend->>Backend: Request designer nonce
Backend->>Printful: Create nonce
Printful-->>Backend: Nonce
Backend-->>Frontend: Nonce
Frontend->>Backend: Template created (templateId)
Backend->>Printful: Fetch variants/details
Printful-->>Backend: Variant data
Backend->>DB: Update POD object
Frontend->>Backend: Generate mockup
Backend->>Printful: Create mockup task
Printful-->>Backend: Mockup result
Backend->>DB: Save mockup URLs
Frontend->>Backend: Place order
Backend->>DB: Fetch POD data
Backend->>Printful: Create order
Printful-->>Backend: Order confirmation
```
## Related
* [Order lifecycle](/guides/order-lifecycle) — where Printful slots into product processing.
* [EasyPost](/guides/integrations/easypost) — the parallel path for non-POD physical goods.
# Stripe
Source: https://docs.droplinked.com/guides/integrations/stripe
Stripe integration — merchant Connect onboarding, customer payment intents, subscription checkout, and webhook handling.
Stripe is Droplinked's default card-payment processor. It powers three flows:
1. **Merchant onboarding** — Connect (Standard accounts) so merchants receive funds in
their own Stripe account
2. **Customer payments** — `PaymentIntent` per order, attached to the merchant's connected
account or to the platform's primary account when the merchant isn't connected
3. **Subscription billing** — Stripe Checkout sessions for Droplinked's own SaaS plans
## Configuration
```env theme={null}
STRIPE_SECRET_KEY=sk_live_... # platform Stripe secret key
STRIPE_WEBHOOK_SECRET=whsec_... # webhook signing secret
```
| Var | Notes |
| ----------------------- | --------------------------------------------------------------------------------------- |
| `STRIPE_SECRET_KEY` | Platform-level secret. Restricted keys with IP allowlist are recommended for production |
| `STRIPE_WEBHOOK_SECRET` | Used to verify webhook payloads from Stripe |
Use **test-mode** keys (`sk_test_...`) for the dev environment. Stripe issues a separate
webhook signing secret per endpoint per environment.
## Merchant onboarding (Stripe Connect Standard)
The platform creates a Standard Connect account for each merchant and returns a hosted
onboarding link.
### Create an account + onboarding link
```ts theme={null}
async createAccount(userId: string, shopId: string): Promise<{ url: string }>
```
Resolve the user's email and the shop's existing `expressStripeAccountId` (if any).
If the shop already has an Express account, throws `BadRequestException` ("already
onboarded").
`stripe.accounts.create({ type: 'standard', email })`. Save the new account ID against
the shop.
`stripe.accountLinks.create()` with a return URL appropriate for the environment.
Return the URL to the merchant.
### Webhook: `account.updated`
The platform listens for `account.updated` events. When `charges_enabled` **and**
`payouts_enabled` are both `true`, the shop's Stripe status is flipped to active.
```ts theme={null}
async onboardWebhookHandler(body: Buffer, sig: string): Promise
```
The handler:
1. Fetches the account-update endpoint secret from config
2. Constructs the event with `stripe.webhooks.constructEvent(body, sig, secret)` —
verifying authenticity
3. If the event is `account.updated` and both capabilities are enabled, marks the shop
active
4. Returns `true` on success; throws `BadRequestException` on verification failure
Always use the raw request body when verifying webhooks. Re-parsed JSON breaks the
signature.
## Customer payments
Per-order payments use the standard `PaymentIntent` flow. See [Order lifecycle](/guides/order-lifecycle)
for how this integrates with the order-confirmation saga.
| Step | Endpoint | What happens |
| ------------- | ----------------------------------------- | --------------------------------------------- |
| Create intent | `POST /v2/orders/:orderId/payment-intent` | Backend calls Stripe; returns `client_secret` |
| Customer pays | Stripe Elements on the storefront | Card charged client-side |
| Webhook | `payment_intent.succeeded` | Order moves to `CONFIRMED` via the saga |
### Connected-account vs platform charges
* **Merchant connected** — `payment_intent.create({ on_behalf_of, transfer_data })` so
funds settle into the merchant's Stripe account; platform retains an application fee
* **Merchant not connected** — Payment captured into the platform's primary Stripe
account; merchant payout reconciled out-of-band
## Subscription billing
Droplinked's own SaaS plans bill through Stripe Checkout (subscription mode). The
subscription gateway is exposed as integration-service endpoints called server-to-server
from the backend.
| Endpoint | Purpose |
| ----------------------- | ------------------------------------------------------------------------ |
| `POST /stripe/checkout` | Create a subscription-mode Checkout Session with on-the-fly `price_data` |
| `POST /stripe/coupon` | Create a one-time coupon (e.g. prorated upgrade credit) |
| `POST /stripe/cancel` | Cancel a subscription |
### Create a subscription checkout session
```json theme={null}
{
"lineItem": {
"planName": "Pro",
"description": "Pro plan, billed monthly",
"priceInDollars": 49.99,
"recurringInterval": "month",
"recurringIntervalCount": 1
},
"metadata": {
"shopId": "...",
"planId": "...",
"flow": "upgrade"
},
"successUrl": "https://droplinked.com/subscribe/success",
"cancelUrl": "https://droplinked.com/subscribe/cancel",
"trialPeriodDays": 0,
"couponId": null
}
```
Returns `{ checkoutUrl, sessionId }`. Redirect the merchant to `checkoutUrl`.
Stripe metadata values must be strings — the gateway drops `null`/`undefined` entries
before sending.
## Webhook signature verification
All webhook handlers (account update, payment-intent succeeded, charge refunded, etc.) use
`stripe.webhooks.constructEvent` to validate the `Stripe-Signature` header against the
endpoint secret. A failed verification returns `400` and is never processed.
For replay safety and the broader webhook test matrix, see
[Checkout stability](/guides/testing/checkout-stability).
## Flow diagram — customer payment
```mermaid theme={null}
sequenceDiagram
participant Customer
participant Frontend
participant Backend
participant Stripe
participant Webhook
Customer->>Frontend: Click "Pay"
Frontend->>Backend: POST /v2/orders/:id/payment-intent
Backend->>Stripe: paymentIntents.create
Stripe-->>Backend: client_secret
Backend-->>Frontend: client_secret
Frontend->>Stripe: confirmPayment (Elements)
Stripe-->>Frontend: succeeded
Stripe->>Webhook: payment_intent.succeeded
Webhook->>Backend: confirmPayment (saga)
Backend->>Backend: Order → CONFIRMED
```
## Troubleshooting
| Symptom | Likely cause |
| --------------------------------------------------- | --------------------------------------------------------------------------------- |
| `signature verification failed` | Wrong webhook secret, or middleware re-parsed the body before verification |
| `account.updated` not arriving | Endpoint not registered for that event, or restricted key blocked at IP allowlist |
| `payment_intent` succeeds but order stays `PENDING` | Webhook reaching `/webhook/stripe`? Check the saga logs; replay safe |
| `403` from Stripe | Restricted key + missing IP allowlist entry for the calling runner |
## Related
* [Order lifecycle](/guides/order-lifecycle)
* [Checkout stability](/guides/testing/checkout-stability)
* [Environment variables](/guides/environment-variables)
# Telr
Source: https://docs.droplinked.com/guides/integrations/telr
Telr — MENA/GCC payment processor. Hosted payment page, SHA1 webhook signature, idempotent state machine, and refunds.
Telr is Droplinked's preferred MENA/GCC payment processor. The integration uses Telr's
**hosted payment page** (PCI scope stays on Telr), forwards transaction-advice webhooks back
to the Droplinked backend, and supports refunds + status polls.
## Architecture
```
┌────────────────────────────────┐
│ Merchant store on Droplinked │
│ (selects Telr in settings) │
└────────────────┬───────────────┘
│
POST /telr/payment │
───────────────────────────▶ │ creates Telr order via Gateway API
│ (store_id + auth_key)
▼
┌────────────────────────────────┐
│ Telr hosted page │
│ (PCI scope stays on Telr) │
└────────────────┬───────────────┘
│ customer pays
▼
POST /telr/webhook │ transaction-advice POST
◀─────────────────────────── │ form-encoded, signed with SHA1
│
GET /telr/transactions/:id/status ← merchant or backend poll
POST /telr/transactions/:id/refund ← merchant-initiated refund
```
Droplinked remains merchant-of-record. Telr never sees the catalog or customer data beyond
what's carried in the order body.
## Endpoints
All endpoints are under `https://apiv3.droplinked.com`.
| Route | Method | Auth | Purpose |
| ------------------------------------------ | ------ | ------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| `/telr/payment` | POST | Merchant JWT | Create a hosted payment session; returns Telr-hosted URL the storefront redirects to |
| `/telr/webhook` | POST | **Signature only** (see below) | Telr → Droplinked transaction-advice. Idempotent, signature-verified. Always replies 200 even on duplicate |
| `/telr/transactions/:transactionId/refund` | POST | Merchant JWT + role check | Initiate a refund. Body: `{ amount: "25.00", reason: "string" }` |
| `/telr/transactions/:transactionId/status` | GET | Merchant JWT | Backend-side status poll (webhook is preferred) |
## Webhook signature scheme
Droplinked verifies the standard Telr advice signature:
```
hash = SHA1( store_id : auth_key : order_ref : amount : currency : status )
```
* Colon-joined, raw bytes, no trailing newline
* Compared with `crypto.timingSafeEqual` (side-channel safe)
* Mismatch returns **HTTP 401** (not 400) so attackers can't probe whether a `cartId` exists
### Required form fields posted to `/telr/webhook`
| Field | Source | Notes |
| ----------- | --------------------- | --------------------------------------------------------------------------------- |
| `store_id` | Telr | Must match `TELR_STORE_ID` |
| `order_ref` | Telr | Echoed back from the original `/telr/payment` request |
| `cart_id` | Droplinked-originated | Optional, useful for fast lookup |
| `amount` | Telr | Decimal string, e.g. `25.00` |
| `currency` | Telr | ISO 4217 (AED, SAR, USD, EUR, …) |
| `status` | Telr | One of `A`, `H`, `P`, `E`, `D`, `C` or `authorised`/`paid`/`declined`/`cancelled` |
| `hash` | Telr | Hex SHA1 of the canonical string above |
### Status mapping
| Telr status | Droplinked `TelrTransactionStatus` |
| ---------------------- | ---------------------------------- |
| `A`, `H`, `authorised` | `AUTHORIZED` |
| `P`, `paid` | `CAPTURED` |
| `E`, `D`, `declined` | `DECLINED` |
| `C`, `cancelled` | `CANCELLED` |
## Idempotency + state-machine guarantees
Telr's webhook handler is fully idempotent. Telr can safely retry on a schedule like
1m / 5m / 30m / 2h / 24h with exponential backoff — we've verified 24h replay safety.
* Every webhook event is persisted by `signature` into `telrTransaction.webhookEventIds`
* State transitions run through a `canTransition` check — **irreversibly forward-only**
* A stale `AUTHORIZED` event arriving after `CAPTURED` is **dropped** (state never rewinds)
* The DB update + idempotency-list push run inside a single transaction so partial failures
don't desync state
## Refund flow
```http theme={null}
POST /telr/transactions/:transactionId/refund
```
```json theme={null}
{
"amount": "25.00",
"reason": "Customer cancelled within 24h"
}
```
* Requires the merchant JWT for the shop that owns the transaction
* Calls Telr's Order API with `ivp_method=refund`, `tran_ref`, `tran_amount`, `tran_currency`
* Successful refunds are recorded as child entries in `telrTransaction.refunds[]`
* **Partial refunds supported** (`amount < captured_amount`); subsequent attempts validate
`cumulative_refunded ≤ captured`
## Test mode
* Toggled by env: `TELR_TEST_MODE=true` (sends `ivp_test=1` on the order body)
* Telr issues a **separate** `store_id` / `auth_key` pair for sandbox; same base URL
(`https://secure.telr.com`)
## Configuration
| Variable | Description |
| -------------------- | ---------------------------------------------- |
| `TELR_STORE_ID` | Issued by Telr (per environment) |
| `TELR_AUTH_KEY` | Held in your secrets manager; rotate quarterly |
| `TELR_BASE_URL` | Defaults to `https://secure.telr.com` |
| `TELR_ENABLED` | Master gate for routing through Telr |
| `TELR_TEST_MODE` | `true` in sandbox, `false` in production |
| `SHOPFRONT_BASE_URL` | Where Telr redirects the customer post-payment |
### Supported currencies
AED, SAR, KWD, BHD, QAR, OMR, USD, EUR. Additional currencies can be enabled in the Telr
merchant portal — Droplinked auto-detects supported currencies from the order-create
response.
## Production checklist
* Telr merchant onboarding complete
* Webhook URL registered with Telr: `https://apiv3.droplinked.com/telr/webhook`
* HMAC verification + idempotency live
* Refund flow tested
* State-machine prevents status rollback
* Sandbox `store_id` + `auth_key` provisioned for `apiv3dev.droplinked.com`
## Related
* [Order lifecycle](/guides/order-lifecycle) — how Telr fits the broader payment flow.
* [Checkout stability](/guides/testing/checkout-stability) — cross-PSP test matrix.
# Trust-Fabric Dashboard Template
Source: https://docs.droplinked.com/guides/integrations/trust-fabric-dashboard-template
Drop-in HTML + JavaScript reference dashboard that polls /v2/trust-fabric/stats and renders aggregate counts. Adapt for your own brand.
A copy-pastable reference dashboard for partners, lenders, and treasury teams who
want to embed droplinked's live trust-fabric scale (and their own activity) into
an internal portal. Sixty-second read; everything below is public, no auth, no SDK.
## Who this is for
* **Lenders + DeFi vaults** displaying their own credit-risk activity alongside the
platform aggregate. See also [DeFi Lender Onboarding](/concepts/for-defi-lenders).
* **Partner portals** signaling platform scale to their internal teams without
exposing per-row data.
* **Treasury teams** evaluating droplinked who want a live read on the trust-fabric
trinity before signing onboarding paperwork.
The dashboard is **read-only** — every endpoint it polls is public + unauthenticated.
No JWT, no IP-allowlist, no SDK install. Drop it onto an internal HTTPS origin and
it works.
## Live data sources
The dashboard polls four public endpoints:
| Endpoint | What it surfaces | Cadence |
| ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------- |
| [`GET /v2/trust-fabric/stats`](/api-reference/public/trust-fabric-stats) | Top-line aggregate (lenders, service providers, methodology versions, attestations by schema) | 60s |
| [`GET /v2/lenders?status=ACTIVE`](/api-reference/public/lender-registry) | Registered lender list (display name, jurisdiction, archetype) | 60s |
| [`GET /v2/methodologies/:lenderId/versions`](/api-reference/public/methodology-registry) | Per-lender methodology depth | on demand |
| [`GET /v2/lender-routing/recommend?jurisdiction=...`](/api-reference/public/lender-routing) | Jurisdiction-ranked lender recommendations | on demand |
`/v2/trust-fabric/stats` has **no server-side cache** in v1 — every call hits the
live aggregate. The recommended client-side polling cadence is **60 seconds or
greater**. The `asOf` field in the response lets you display data freshness
honestly even with stale-while-revalidate strategies.
## Dashboard code
Self-contained — save as `dashboard.html`, open in any browser.
```html theme={null}
Droplinked Trust-Fabric Dashboard
);
}
```
## CORS
All four endpoints above are CORS-permissive — they respond with
`Access-Control-Allow-Origin: *` to browser fetches from any origin. You can verify
locally:
```bash theme={null}
curl -sI -H "Origin: https://yourdomain.example" \
https://apiv3.droplinked.com/v2/trust-fabric/stats | grep -i access-control
```
If you encounter CORS issues from a specific origin, email `support@droplinked.com`
with the origin + the failing request — it likely indicates a WAF rule, not a CORS
policy.
## What to customize
The reference dashboard is intentionally minimal. Common customizations:
* **Brand chrome** — swap the `#2bcfa1` / `#06c295` palette for your own; replace
the `
` with your logo.
* **Filter to your own lenderId** — replace the `/v2/lenders?status=ACTIVE` poll
with a single-row `/v2/lenders/:lenderId` lookup and show only your own
activity.
* **Per-methodology version history** — add a click handler on each lender row
that opens `/v2/methodologies/:lenderId/versions` and renders the timeline.
* **Jurisdiction routing widget** — add an input + button calling
`/v2/lender-routing/recommend?jurisdiction=...` and render the ranked
recommendations.
* **Aggregate trend** — store the `asOf` + counts in a small client-side ring
buffer (e.g. `localStorage`) and render a 24h sparkline of Schema B growth.
## Caching
Recommend a **client-side cache of 60 seconds or greater**. The
`/v2/trust-fabric/stats` endpoint has no server-side cache in v1 — every call hits
the live aggregate. The other endpoints (`/v2/lenders`, `/v2/methodologies/:lenderId/versions`,
`/v2/lender-routing/recommend`) likewise execute live; treat them the same.
If you're polling at higher cadence than 60s, layer a service-worker cache or
React Query / SWR with `staleTime: 60_000` to keep partner dashboards responsive
without unnecessary load on the apiv3 origin.
## Related
* [Trust-Fabric Stats](/api-reference/public/trust-fabric-stats) — endpoint reference
* [DeFi Lender Onboarding](/concepts/for-defi-lenders) — full lender onboarding narrative
* [Trust Fabric (EAS Schema v2)](/concepts/trust-fabric) — 4-axis architecture overview
* [Lender Registry Lookup](/api-reference/public/lender-registry) — `/v2/lenders/*` reference
* [Methodology Registry Lookup](/api-reference/public/methodology-registry) — `/v2/methodologies/*` reference
# API cookbook
Source: https://docs.droplinked.com/guides/library/api-cookbook
Recipes for building with the Droplinked headless APIs — start with a complete custom-store reference build in JavaScript and Python.
A developer's guide to integrating the Droplinked APIs for seamless store and app creation.
Each recipe is a complete, end-to-end walkthrough you can copy into your own project.
From auth and shop discovery through cart, shipping, payment, and order creation — with
full JavaScript and Python implementations.
***
## Build a custom store with Droplinked headless APIs
Learn how to build a fully custom store directly on your platform or app using Droplinked's
headless APIs for seamless inventory and checkout management.
### Prerequisites
Before you begin, you'll need:
1. A **Droplinked account** — sign up at [droplinked.com](https://droplinked.com)
2. An **API key** — your authentication credential for API requests
3. A **shop name** — your unique shop identifier
4. Basic development knowledge (or a developer to help implement)
### Get your API key
Go to [droplinked.com](https://droplinked.com) and sign in.
Navigate to **Settings → API Keys**.
Click **Generate New API Key** and copy the value securely.
Never share your API key publicly or commit it to version control.
### Base configuration
```bash theme={null}
# Base URL
https://api.io.droplinked.com
# Required headers for every request
x-droplinked-api-key: YOUR_API_KEY
Content-Type: application/json
```
#### Example — `curl`
```bash theme={null}
curl -X GET "https://api.io.droplinked.com/shops/v2/public/name/lumen" \
-H "x-droplinked-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json"
```
***
### Step 1 — Fetch shop information
Get your shop details to display branding on the storefront.
```bash theme={null}
GET https://api.io.droplinked.com/shops/v2/public/name/{shopName}
```
Sample response:
```json theme={null}
{
"_id": "69103f6e530855f4b75c4da2",
"id": "69103f6e530855f4b75c4da2",
"name": "Lumen",
"url": "lumen",
"description": "We believe light is an art form. We curate sculptural lamps and architectural lighting designed to transform your space.",
"currency": {
"abbreviation": "USD",
"symbol": "$",
"conversionRateToUSD": 1
}
}
```
Use this to render the shop name, description, and currency formatting on your homepage.
***
### Step 2 — List your products
Fetch all products to populate the catalog.
```bash theme={null}
GET https://api.io.droplinked.com/product-v2/public/shop/{shopName}
```
Sample response:
```json theme={null}
{
"data": [
{
"id": "6911a2390b1483ce323968e0",
"title": "The Altar Table Lamp",
"slug": "the-altar-table-lamp-f11e0",
"type": "physical",
"status": "published",
"images": [
{
"original": "https://upload-file-droplinked.s3.amazonaws.com/28cefe88...png",
"thumbnail": "https://upload-file-droplinked.s3.amazonaws.com/28cefe88..._small.png",
"alt": "031_Output_Lamp.png"
}
],
"thumbnail": "https://upload-file-droplinked.s3.amazonaws.com/28cefe88..._small.png",
"isPurchasable": true,
"lowestPrice": 899
}
],
"currentPage": 1,
"totalPages": 1,
"totalDocuments": 10
}
```
The `lowestPrice` shown here is for display only. To add items to a cart you need the **SKU ID**
from the product details endpoint (step 3).
***
### Step 3 — Get product details (and the SKU)
When a customer clicks a product, fetch the complete details including SKUs.
```bash theme={null}
GET https://api.io.droplinked.com/product-v2/public/{productId}
```
Sample response:
```json theme={null}
{
"id": "6911a2390b1483ce323968e0",
"title": "The Altar Table Lamp",
"description": "A stunning sculptural lamp...",
"images": [
{
"original": "https://upload-file-droplinked.s3.amazonaws.com/...",
"thumbnail": "https://upload-file-droplinked.s3.amazonaws.com/..._small.png"
}
],
"skus": [
{
"id": "sku_lamp_abc123",
"price": 899,
"currency": "USD",
"inventory": {
"quantity": 10,
"available": true
}
}
]
}
```
**Product vs SKU**
* A **product** is the general item (e.g. "Blue T-Shirt")
* A **SKU** is a specific variant with its own price (e.g. "Blue T-Shirt — Size Large — \$29.99")
* A product can have many SKUs (sizes, colors, etc.)
* You add the **SKU** to the cart, not the product
***
### Step 4 — Create a cart
```bash theme={null}
POST https://api.io.droplinked.com/v2/carts
```
Body:
```json theme={null}
{ "shopId": "69103f6e530855f4b75c4da2" }
```
Response (excerpt):
```json theme={null}
{
"id": "6927faf705c8819a2dd672ae",
"shopId": "69103f6e530855f4b75c4da2",
"status": "ACTIVE",
"items": [],
"financialDetails": { "amounts": { "totalAmount": 0 } },
"expiredAt": "2025-11-28T07:17:11.570Z",
"createdAt": "2025-11-27T07:17:11.571Z"
}
```
Save the cart ID in `localStorage` (browser) or a session (server) so customers can continue
shopping after a refresh.
***
### Step 5 — Add product to cart
Add items using the **SKU ID** (not the product ID).
```bash theme={null}
POST https://api.io.droplinked.com/v2/carts/{cartId}/products
```
Body:
```json theme={null}
{
"skuId": "sku_lamp_abc123",
"quantity": 1
}
```
Optional `m2m_data` for made-to-measure / customized products:
```json theme={null}
{
"skuId": "sku_custom_item",
"quantity": 1,
"m2m_data": {
"position": "front",
"artworkUrl": "https://example.com/custom-design.png"
}
}
```
### Update or remove items
| Method | Endpoint | Purpose |
| -------- | ------------------------------------- | ------------------------------------------- |
| `GET` | `/v2/carts/{cartId}` | View cart contents |
| `PATCH` | `/v2/carts/{cartId}/products/{skuId}` | Update quantity (body: `{ "quantity": 3 }`) |
| `DELETE` | `/v2/carts/{cartId}/products/{skuId}` | Remove an item |
***
### Step 6 — Checkout — shipping, payment, order
#### Get shipping rates
```bash theme={null}
GET https://api.io.droplinked.com/v2/carts/{cartId}/shipping
```
#### Select a rate
```bash theme={null}
POST https://api.io.droplinked.com/v2/carts/{cartId}/shipping
```
Body:
```json theme={null}
{
"selectedShippingRate": [
{ "shipmentId": "shipment_001", "rateId": "rate_standard" }
]
}
```
#### List payment methods
```bash theme={null}
GET https://api.io.droplinked.com/v2/carts/{cartId}/payment-methods
```
#### Create the order
```bash theme={null}
POST https://api.io.droplinked.com/v2/orders
```
Body:
```json theme={null}
{ "cartId": "6927faf705c8819a2dd672ae" }
```
Response (excerpt):
```json theme={null}
{
"success": true,
"data": {
"orderId": "order_final_123",
"total": 904.99,
"status": "pending_payment",
"items": [ { "title": "The Altar Table Lamp", "quantity": 1, "price": 899 } ],
"shipping": 5.99
}
}
```
***
### Complete reference implementations
```javascript JavaScript / TypeScript theme={null}
const API_KEY = "your-api-key-here";
const SHOP_NAME = "lumen";
const BASE_URL = "https://api.io.droplinked.com";
async function callAPI(endpoint, method = "GET", body = null) {
const options = {
method,
headers: {
"x-droplinked-api-key": API_KEY,
"Content-Type": "application/json"
}
};
if (body) options.body = JSON.stringify(body);
const response = await fetch(`${BASE_URL}${endpoint}`, options);
return response.json();
}
async function getShop() { return callAPI(`/shops/v2/public/name/${SHOP_NAME}`); }
async function getProducts() { return callAPI(`/product-v2/public/shop/${SHOP_NAME}`); }
async function getProductDetails(id) { return callAPI(`/product-v2/public/${id}`); }
async function createCart(shopId) { return callAPI("/v2/carts", "POST", { shopId }); }
async function addToCart(cartId, skuId, quantity = 1) {
return callAPI(`/v2/carts/${cartId}/products`, "POST", { skuId, quantity });
}
async function getCart(cartId) { return callAPI(`/v2/carts/${cartId}`); }
async function updateQuantity(cartId, skuId, quantity) {
return callAPI(`/v2/carts/${cartId}/products/${skuId}`, "PATCH", { quantity });
}
async function removeFromCart(cartId, skuId) {
return callAPI(`/v2/carts/${cartId}/products/${skuId}`, "DELETE");
}
async function getShipping(cartId) { return callAPI(`/v2/carts/${cartId}/shipping`); }
async function selectShipping(cartId, shipmentId, rateId) {
return callAPI(`/v2/carts/${cartId}/shipping`, "POST", {
selectedShippingRate: [{ shipmentId, rateId }]
});
}
async function getPaymentMethods(cartId) { return callAPI(`/v2/carts/${cartId}/payment-methods`); }
async function createOrder(cartId) { return callAPI("/v2/orders", "POST", { cartId }); }
async function completeShoppingFlow() {
try {
const shop = await getShop();
const productsData = await getProducts();
const products = productsData.data;
const product = await getProductDetails(products[0].id);
const skuId = product.skus[0].id;
const cartData = await createCart(shop.id);
const cartId = cartData.id;
await addToCart(cartId, skuId, 1);
const shipping = await getShipping(cartId);
await selectShipping(cartId, shipping.data[0].shipmentId, shipping.data[0].rateId);
const order = await createOrder(cartId);
console.log("Order created:", order.data.orderId);
} catch (error) {
console.error("Error:", error);
}
}
completeShoppingFlow();
```
```python Python theme={null}
import requests
API_KEY = "your-api-key-here"
SHOP_NAME = "lumen"
BASE_URL = "https://api.io.droplinked.com"
class DroplinkedAPI:
def __init__(self, api_key, shop_name):
self.api_key = api_key
self.shop_name = shop_name
self.headers = {
"x-droplinked-api-key": api_key,
"Content-Type": "application/json"
}
def get_shop(self):
return requests.get(
f"{BASE_URL}/shops/v2/public/name/{self.shop_name}",
headers=self.headers
).json()
def get_products(self):
return requests.get(
f"{BASE_URL}/product-v2/public/shop/{self.shop_name}",
headers=self.headers
).json()
def get_product_details(self, product_id):
return requests.get(
f"{BASE_URL}/product-v2/public/{product_id}",
headers=self.headers
).json()
def create_cart(self, shop_id):
return requests.post(
f"{BASE_URL}/v2/carts",
headers=self.headers,
json={"shopId": shop_id}
).json()
def add_to_cart(self, cart_id, sku_id, quantity=1):
return requests.post(
f"{BASE_URL}/v2/carts/{cart_id}/products",
headers=self.headers,
json={"skuId": sku_id, "quantity": quantity}
).json()
def get_cart(self, cart_id):
return requests.get(
f"{BASE_URL}/v2/carts/{cart_id}",
headers=self.headers
).json()
def update_quantity(self, cart_id, sku_id, quantity):
return requests.patch(
f"{BASE_URL}/v2/carts/{cart_id}/products/{sku_id}",
headers=self.headers,
json={"quantity": quantity}
).json()
def remove_from_cart(self, cart_id, sku_id):
return requests.delete(
f"{BASE_URL}/v2/carts/{cart_id}/products/{sku_id}",
headers=self.headers
).json()
def get_shipping(self, cart_id):
return requests.get(
f"{BASE_URL}/v2/carts/{cart_id}/shipping",
headers=self.headers
).json()
def select_shipping(self, cart_id, shipment_id, rate_id):
return requests.post(
f"{BASE_URL}/v2/carts/{cart_id}/shipping",
headers=self.headers,
json={"selectedShippingRate": [
{"shipmentId": shipment_id, "rateId": rate_id}
]}
).json()
def get_payment_methods(self, cart_id):
return requests.get(
f"{BASE_URL}/v2/carts/{cart_id}/payment-methods",
headers=self.headers
).json()
def create_order(self, cart_id):
return requests.post(
f"{BASE_URL}/v2/orders",
headers=self.headers,
json={"cartId": cart_id}
).json()
def main():
api = DroplinkedAPI(API_KEY, SHOP_NAME)
shop = api.get_shop()
products = api.get_products()["data"]
product = api.get_product_details(products[0]["id"])
sku_id = product["skus"][0]["id"]
cart = api.create_cart(shop["id"])
cart_id = cart["id"]
api.add_to_cart(cart_id, sku_id, 1)
shipping = api.get_shipping(cart_id)
rate = shipping["data"][0]
api.select_shipping(cart_id, rate["shipmentId"], rate["rateId"])
order = api.create_order(cart_id)
print(f"Order created: {order['data']['orderId']}")
if __name__ == "__main__":
main()
```
***
### Best practices
#### 1. Error handling
Wrap every API call in `try / catch` (or its language equivalent) and surface user-friendly
messages:
```js theme={null}
try {
const shop = await getShop();
} catch (error) {
console.error("Failed to fetch shop:", error);
}
```
#### 2. Cart persistence
Store the cart ID so customers can resume:
```js theme={null}
// Save
localStorage.setItem("cart_id", cartId);
// Retrieve
const cartId = localStorage.getItem("cart_id");
if (cartId) {
const cart = await getCart(cartId);
}
```
#### 3. Loading states
Show loading indicators while fetching data — the public endpoints can take a few hundred
milliseconds on first load.
#### 4. Inventory checking
Always confirm a SKU is available before adding to cart:
```js theme={null}
const product = await getProductDetails(productId);
const sku = product.skus[0];
if (sku.inventory.available && sku.inventory.quantity > 0) {
await addToCart(cartId, sku.id, 1);
} else {
alert("This item is out of stock");
}
```
#### 5. Price formatting
Use the platform's `Intl.NumberFormat` and the shop's currency:
```js theme={null}
const formatPrice = (price, currency) =>
new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(price);
formatPrice(899, 'USD'); // "$899.00"
```
***
### Troubleshooting
| Symptom | Likely cause |
| ------------------------ | ---------------------------------------------------------------------------------- |
| `API key not configured` | Wrong header name (must be exactly `x-droplinked-api-key`) or revoked key |
| `Cart ID not found` | Cart expired — check `expiredAt`; create a new cart |
| `Product not found` | Product ID wrong, not published, or doesn't belong to your shop |
| No products returned | Shop name wrong, products unpublished, or API key lacks access |
| Can't add to cart | Using the **product** ID instead of the **SKU** ID; SKU out of stock; cart expired |
***
### Endpoint quick reference
| Endpoint | Method | Purpose |
| ----------------------------------- | ------ | --------------------- |
| `/shops/v2/public/name/:name` | GET | Get shop info |
| `/product-v2/public/shop/:shopName` | GET | List products |
| `/product-v2/public/:id` | GET | Product details + SKU |
| `/v2/carts` | POST | Create cart |
| `/v2/carts/:cartId` | GET | Cart contents |
| `/v2/carts/:cartId/products` | POST | Add to cart |
| `/v2/carts/:cartId/products/:skuId` | PATCH | Update quantity |
| `/v2/carts/:cartId/products/:skuId` | DELETE | Remove from cart |
| `/v2/carts/:cartId/shipping` | GET | Get shipping rates |
| `/v2/carts/:cartId/shipping` | POST | Select shipping |
| `/v2/carts/:cartId/payment-methods` | GET | Get payment methods |
| `/v2/orders` | POST | Create order |
## Related
* [API overview](/guides/library/api-overview) — request shape, auth, base URL
* [Use cases](/guides/library/use-cases) — endpoint reference by surface
* [Web3 shop tutorial](/guides/library/web3-shop-tutorial) — alternative tutorial-style walkthrough
* Live [API Reference](/api-reference/introduction) — interactive OpenAPI browser
For one-on-one help, email [support@droplinked.com](mailto:support@droplinked.com).
# Droplinked API
Source: https://docs.droplinked.com/guides/library/api-overview
Introduction to the Droplinked API — a comprehensive suite of endpoints for building modern, scalable eCommerce systems with Web3-native features.
The **Droplinked API** is a collection of powerful endpoints that allow developers to build
complete eCommerce platforms or store-management applications. With Droplinked, you can easily
create online stores, manage product inventories, handle payments — including **Web3 crypto
transactions** — and even **mint products as NFTs**.
It provides everything you need to build a modern, connected, and scalable eCommerce
experience.
## Key features
Create and manage multiple stores with full configuration control.
Add, update, and organize products or inventory items.
Integrate decentralized payments using crypto and blockchain technology.
Convert products into NFTs to enable on-chain ownership or digital verification.
## What you can build
Developers use the Droplinked API to build:
* **Online stores** — create and manage digital storefronts
* **Admin dashboards** — build management panels for merchants and sellers
* **Web3 eCommerce apps** — enable features such as crypto payments and NFT product minting
## Getting started
### 1. Get your API key
To start using the API, create an API key from your **Droplinked dashboard**.
Go to [droplinked.com](https://droplinked.com) and log in with your account credentials.
Open **Settings → Developers** in the side navigation.
In the **API Keys** tab, create a new key. Copy the value — it won't be shown again.
Keep your API key secure — it identifies and authenticates your requests to Droplinked's
backend. Never commit it to source control or expose it in client-side bundles.
### 2. Base URL
All API requests should be made to:
```bash theme={null}
https://api.io.droplinked.com/
```
### 3. Authentication header
Every request must include your API key in the headers:
```bash theme={null}
x-droplinked-api-key: YOUR_API_KEY
```
#### Example — `curl`
```bash theme={null}
curl -X GET "https://api.io.droplinked.com/v1/store" \
-H "x-droplinked-api-key: YOUR_API_KEY"
```
### 4. Live API reference
You can explore and test the full API specification interactively from the
[API Reference](/api-reference/introduction) tab in this docs site, or browse the legacy Swagger
at [apiv3.droplinked.com/swagger/dev-docs](https://apiv3.droplinked.com/swagger/dev-docs).
The OpenAPI spec is downloadable at
[apiv3.droplinked.com/swagger/json](https://apiv3.droplinked.com/swagger/json).
## Core concepts
The main functional modules in the Droplinked API:
| Module | Purpose |
| --------------------------- | ---------------------------------------------------------------------- |
| **User management** | Merchant registration, authentication (login), and account management |
| **Product management** | Create, edit, and organize products within stores |
| **Shipping management** | Configure and manage shipping systems and delivery methods |
| **Cart & Order management** | Cart creation, order placement, and order tracking |
| **Payment management** | Process and manage payments through traditional and Web3 methods |
| **Blog management** | Create and manage blogs, articles, and related content for storefronts |
These entities connect together as shown in the [platform model](/concepts/platform-model)
overview.
## API reference catalog
The full live API reference (auto-generated from OpenAPI) groups endpoints by resource:
Authentication, registration, profileCustomer accounts + sessionsShop CRUD, payment methods, domainsProduct CRUD + variantsProduct groupingsStock-keeping units + inventoryRates + carriersGift-card issuance + redemptionArticles + contentCart lifecycleOrder placement + trackingSaved shipping addresses
Bookmark the [API Reference](/api-reference/introduction) tab to always have the latest version
of Droplinked's interactive endpoint documentation handy.
## Related
* [Use cases](/guides/library/use-cases) — endpoint-by-endpoint walkthroughs
* [API cookbook](/guides/library/api-cookbook) — end-to-end recipes
* [Web3 shop tutorial](/guides/library/web3-shop-tutorial) — build a full storefront
* [Droplinked tools](/guides/library/droplinked-tools) — embeddables, OAuth, DIMST
# Droplinked tools
Source: https://docs.droplinked.com/guides/library/droplinked-tools
A repository of the different tools and libraries available across the Droplinked protocol — toolkits, embeddable units, OAuth, and the API access key.
Learn how to use Droplinked's tools and endpoints to unlock the power of Web3 commerce. Here
you'll find documentation for the tools necessary to interact with the Droplinked protocol —
geared towards users, system administrators, and technical partners wanting to understand and
integrate with the protocol.
## Developer toolkit
The Web3 libraries and tools below provide the base infrastructure required for Droplinked to
operate on any given chain.
Fractionalize product NFTs so each fraction represents one purchasable unit.
Customizable storefronts built on Next.js — a reference implementation against the
Droplinked APIs.
Embeddable units that place purchasable inventory anywhere online (sites, apps,
marketplaces).
Decentralized Inventory Management and Sales Tracking — indexer by chain of choice.
Native payment with any ERC-20, SPL (Solana), or BRC-20 token.
Report issues or recommend new protocol features and functions.
## Interacting with the protocol
### OAuth
Use crypto-wallet credentials to authenticate and interact with the Droplinked protocol.
Wallet-based OAuth removes friction for both merchants and customers — and keeps the
authorization surface entirely Web3-native.
### NextJS sample project — customizable shopfronts
To improve the customer experience and enable developers to create new commerce concepts and
Web3 innovation across Droplinked, the shopfront APIs are exposed for third-party rendering.
These can be integrated with any current CMS and tooling to support new shopfront concepts that
can be built with different technologies (virtual showrooms, gaming/metaverse shopfronts, etc.).
Next.js is a robust framework that offers numerous advantages for e-commerce development,
particularly when used in conjunction with the Droplinked APIs. Its performance optimization,
SEO benefits, scalability, security features, and developer-friendly environment make it an
attractive choice for businesses looking to build or enhance their online storefronts.
You can access the reference repo here:
[github.com/droplinked/Next-ShopFront](https://github.com/droplinked/Next-ShopFront).
### API access key
In the merchant dashboard, click the **settings** icon (gear) on the left navigation bar and
scroll to the **API Key** section, where you can generate your own unique key to authenticate
calls to the public APIs.
Navigate to **Settings → API Keys** in your Droplinked merchant dashboard.
Click **Generate New API Key**. Copy the value immediately — it won't be shown again.
Pass the value as `x-droplinked-api-key` on every authenticated request. See
[API overview](/guides/library/api-overview) for the request shape.
You can review the live OpenAPI surface from the [API Reference](/api-reference/introduction)
tab in this docs site, or the legacy Swagger:
[apiv3.droplinked.com/v1/public-apis/document](https://apiv3.droplinked.com/v1/public-apis/document).
### DIMST Console
A CLI and UI for simple execution of product registration, inventory management, sales
tracking, and batch function/registration operations.
### Product Tiles — embeddables
Headless embeddable units for inventory display, allowing both producers and publishers to
quickly and easily embed Droplinked-registered inventory across their properties — websites,
apps, or marketplaces.
### Tokenpay
Tokenpay by Droplinked empowers token issuers and holders to use minted digital assets as a
seamless token payment gateway. The functionality enables buyers to use these tokens to
purchase both physical and digital goods. It helps foster community support with a reward
system while boosting token adoption and engagement —
[learn more](https://github.com/droplinked/tokenpay).
### Issues and support
To report any issues you discover across the Droplinked ecosystem, file them in the
[issues repo](https://github.com/droplinked/issues/issues).
For one-on-one support or recommendations, contact
[support@droplinked.com](mailto:support@droplinked.com).
## Network support
The tools above are available on:
* Ethereum
* Solana
* NEAR
* Polygon
* Base
* Hedera
* XRPL (EVM Sidechain)
* Casper
* SKALE
* BNB
* Stacks
More networks come online based on extended support for volume-based registration of primary
goods alongside RWA secondaries. Deployment on each of these networks allows for commerce
settlement in the native token for each respective network, and serves as the foundation for
cross-chain interoperability bridging into Droplinked's protocol.
Droplinked 1.0 testnet development is underway. Droplinked 1.0 ties together the base
infrastructure for a holistic cross-chain protocol — complete with token contracts, the
Droplinked treasury, and staking functionality.
## Related
* [How Droplinked works](/guides/library/overview)
* [Droplinked API overview](/guides/library/api-overview)
* [API cookbook — Build a custom store](/guides/library/api-cookbook)
* [Web3 shop tutorial](/guides/library/web3-shop-tutorial)
# How Droplinked works
Source: https://docs.droplinked.com/guides/library/overview
An overview of how the Droplinked protocol operates as a network — on-chain product registration, authenticated distribution, trustless settlement, and network yield.
Droplinked combines four critical functions into a single protocol to simplify Web3 commerce for
producers and publishers. The protocol streamlines inventory management and sales tracking for
producers, disintermediates legacy affiliate networks and middlemen eating into margins, and
incentivizes contributors through earned ownership of the network.
Producers register inventory as NFTs on the chain of their choice. Each listing and sale is
verifiable on-chain with full context (color, size, description, quantity, price,
commission, royalty payout).
Producers identify and authorize publishers to sell their listings, with pre-defined
contract terms enforced by smart contracts on the Droplinked network.
Sales settlements are recorded on-chain with immutable splits to producers, owners,
distributors, affiliates, and partners — settled via Proof of Conversion.
A 1% protocol fee accrues to the Droplinked treasury. Contributors who hold staked DROPS
earn yield from the growth of gross network volume.
## Decentralized registration of products
Producers register inventory into listings on their blockchain of choice. Each item listed and
sale generated is verifiable on-chain with context as to the variables associated with that
product listing — color, size, description, quantity, price, commission, royalty payout, etc.
Once a product's attributes have been defined, an NFT is minted for each listing and associated
with a SKU. This inventory is then fractionalized using
[Droplinked's NFT Fractionalizer](https://fractionalizer.droplinked.com),
in which each NFT fraction represents one product unit from those that had been registered.
## Authenticated distribution
Producers identify and authorize publishers to sell and distribute their listings while giving
partners the power to select counterparties and outline pre-defined contract terms for
affiliated sales. Because these terms are defined through smart contracts on the Droplinked
network, the distribution process is simplified with maximum transparency and a trustless
settlement guarantee based on Proof of Conversion.
Through authenticated publishers, partners can quickly and strategically deploy inventory
listings across multiple properties while leveraging unique benefits such as token-gating,
loyalty programs, and custom campaigns or offers.
## Trustless settlement
There are numerous participants across the commerce supply chain. Droplinked simplifies payment
to contributors by recording all sales settlements on-chain with immutable splits defined to
producers, owners, distributors, affiliates, and partners.
True contributors benefit from transparent settlement with the assurance that their payouts are
immutably disbursed based on validated contributions. No longer are there inefficient third-party
processes delaying payout settlements. This also resolves fraudulent sales attribution
infamously associated with legacy affiliate infrastructure by leveraging smart product contracts
for every product listing.
With every sale attributed through Proof of Conversion, the Droplinked network takes a **1%
protocol fee** that is automatically deposited into the Droplinked treasury.
## Network yield
1% of the value of every validated sale is automatically deposited as a network fee and
reserved into the Droplinked treasury. Additionally, producers and publishers can choose to
receive a portion of their settlement value in **DROPS tokens** to stake towards future GMV
yield payouts. For whatever portion they choose to receive in DROPS, a proportionate amount of
the fiat value of that settlement is reserved in the Droplinked treasury.
The treasury continues to accumulate the protocol fee through the growth of the network as
gross network volume sales increase. Contributors are entitled to a network yield through staked
DROPS — a mechanism for rewarding early adopters as participants who can generate a yield from
the future growth of the network.
## What's next
NFT Fractionalizer, NextJS sample storefront, Product Tiles, DIMST, Tokenpay, OAuth, and
more.
Headless commerce APIs for stores, products, carts, checkout, and Web3 payments.
End-to-end recipes — build a custom store with Droplinked's headless APIs.
Endpoint walkthroughs for user, store, product/collection, and cart/checkout management.
# Use cases
Source: https://docs.droplinked.com/guides/library/use-cases
Endpoint-by-endpoint walkthroughs for the most common Droplinked surfaces — user management, store management, product & collection management, and cart & checkout.
A curated tour of the Droplinked API surfaces you'll touch most often when building a custom
storefront, dashboard, or integration. All routes are documented in full in the live
[API Reference](/api-reference/introduction).
Register, log in, refresh tokens, recover passwords.
Create stores, manage payment methods, expose public store data.
Publish products, manage SKUs, organize into collections.
Build the cart, apply coupons, select shipping, place orders, take payment.
***
## User management
Endpoints for user registration, login, password recovery, and token refresh. Use these routes
when building applications that require **merchant** authentication.
| Method | Endpoint | Purpose |
| ------ | --------------------------- | --------------------------------------------------------- |
| `POST` | `/merchant/register` | Registers a new merchant account on Droplinked |
| `POST` | `/merchant/login` | Authenticates an existing merchant; returns access tokens |
| `POST` | `/merchant/forgot-password` | Sends a password-reset link to the merchant's email |
| `POST` | `/merchant/refresh` | Generates a new access token from a valid refresh token |
***
## Store management
These endpoints allow merchants to create, configure, and manage their stores. They cover store
setup, updating details, managing payment methods, and retrieving store data both privately
and publicly.
| Method | Endpoint | Purpose |
| ------- | ------------------------------ | --------------------------------------------------------------------------------------- |
| `GET` | `/shops/v2/check-url` | Check if a chosen store URL (subdomain or slug) is available |
| `POST` | `/shops/v2/setup` | Initial store setup — initialize a new store with basic configuration and owner details |
| `GET` | `/shops/v2` | Retrieve all details of the currently logged-in merchant's store |
| `PATCH` | `/shops/v2` | Update store configuration — name, description, branding, general settings |
| `PUT` | `/shops/v2/payment-methods` | Add or update available payment methods for the store |
| `GET` | `/shops/v2/payment-methods` | List all payment methods currently configured for the store |
| `GET` | `/shops/v2/public/{id}` | Fetch public store information by ID — no auth required |
| `GET` | `/shops/v2/public/name/{name}` | Fetch store information by its unique name / handle |
| `GET` | `/shops/v2/domain/{domain}` | Retrieve a store's public information by its custom domain |
The `public/*` endpoints don't require authentication and are intended for use on storefronts,
landing pages, and any unauthenticated surface where a shop is being displayed.
***
## Product and collection management
A **collection** is a logical grouping of related products (e.g. "Summer Sale", "New Arrivals").
Every product must belong to a collection — this is what powers storefront navigation and
discoverability.
### Required fields when publishing a product
* `title`
* `description`
* `image`
* `collection`
* `status`
* `skus`
* `price` for each SKU
* inventory for each SKU
* `shippingMethodId` *(only for physical products)*
If the product is **physical**, a **shipping method ID** must be provided. A **collection ID**
is always required.
### Product endpoints
| Method | Endpoint | Purpose |
| -------- | ------------------------------------ | --------------------------------------------------------- |
| `GET` | `/product-v2` | List all products for the authenticated merchant |
| `POST` | `/product-v2` | Create a new product under an existing collection |
| `GET` | `/product-v2/public/shop/{shopName}` | List all public products of a shop — no auth |
| `GET` | `/product-v2/public/by-slug/{slug}` | Fetch a public product by its SEO-friendly slug |
| `GET` | `/product-v2/public/{id}` | Fetch a public product by ID |
| `GET` | `/product-v2/{id}` | Fetch product details for the authenticated merchant |
| `PATCH` | `/product-v2/{id}` | Update product fields (title, description, price, images) |
| `DELETE` | `/product-v2/{id}` | Delete a product — requires auth, cannot be undone |
***
## Cart and checkout
This section walks through creating and managing a shopping cart, adding products, attaching
a customer, setting shipping, applying coupons, and finally creating and paying for an order.
`POST /v2/carts` — creates a new cart within a specific store. Optionally, a customer ID
or email can be attached at creation time.
`POST /v2/carts/{cartId}/products` — add a product by SKU ID and quantity.
`GET /v2/carts/{cartId}` for contents. `DELETE /v2/carts/{cartId}` to clear.
`DELETE /v2/carts/{cartId}/products/{skuId}` removes a single line.
`PATCH /v2/carts/{cartId}/products/{skuId}` updates quantity.
`PATCH /v2/carts/{cartId}/customer` — associates a customer with the cart.
First `POST /address-book` to create the address, then
`PATCH /v2/carts/{cartId}/details` to link it to the cart.
`GET /v2/carts/{cartId}/shipping` returns available methods for the address.
`POST /v2/carts/{cartId}/shipping` selects one.
`POST /v2/carts/{cartId}/coupon` — apply a discount / promo code.
`GET /v2/carts/{cartId}/payment-methods` — returns all options available for the cart
(Stripe, crypto, regional PSPs, etc.).
`POST /v2/orders` — creates an order from the finalized cart. The returned order ID is used
in the payment step.
Install `@droplinked/payment-intent` and mount `` with the
`orderId` and `type` (e.g. `stripe`, `USDT-BINANCE`).
### Endpoint reference
| Method | Endpoint | Purpose |
| -------- | ------------------------------------- | ------------------------------- |
| `POST` | `/v2/carts` | Create a cart |
| `POST` | `/v2/carts/{cartId}/products` | Add a product |
| `GET` | `/v2/carts/{cartId}` | Retrieve cart contents |
| `DELETE` | `/v2/carts/{cartId}` | Remove a cart |
| `DELETE` | `/v2/carts/{cartId}/products/{skuId}` | Remove an item from cart |
| `PATCH` | `/v2/carts/{cartId}/products/{skuId}` | Update item quantity |
| `PATCH` | `/v2/carts/{cartId}/customer` | Attach customer to cart |
| `POST` | `/address-book` | Create an address |
| `PATCH` | `/v2/carts/{cartId}/details` | Attach an address to the cart |
| `GET` | `/v2/carts/{cartId}/shipping` | List available shipping methods |
| `POST` | `/v2/carts/{cartId}/shipping` | Select a shipping method |
| `POST` | `/v2/carts/{cartId}/coupon` | Apply a coupon |
| `GET` | `/v2/carts/{cartId}/payment-methods` | List payment methods |
| `POST` | `/v2/orders` | Create the order |
### Payment SDK
Install the payment package:
```bash theme={null}
npm install @droplinked/payment-intent
```
#### Stripe (card) example
```jsx theme={null}
{
console.log('Payment successful');
}}
onCancel={() => {
console.log('Payment cancelled');
}}
onError={(error) => {
console.error('Error:', error);
}}
/>
```
#### Crypto (USDT on Binance) example
```jsx theme={null}
{
console.log('USDT payment successful');
window.location.href = '/success';
}}
onCancel={() => {
console.log('USDT payment cancelled');
window.location.href = '/cancel';
}}
onError={(error) => {
console.error('USDT payment error:', error);
alert('Payment failed. Please try again.');
}}
commonStyle={{
theme: 'dark',
fontFamily: 'system-ui, sans-serif',
colorPrimary: '#F0B90B'
}}
/>
```
## Related
* [API overview](/guides/library/api-overview) — auth, base URL, core concepts
* [API cookbook](/guides/library/api-cookbook) — end-to-end recipe in JS + Python
* [Web3 shop tutorial](/guides/library/web3-shop-tutorial) — full storefront walkthrough
* Live [API Reference](/api-reference/introduction) — interactive endpoint browser
# Web3 shop tutorial
Source: https://docs.droplinked.com/guides/library/web3-shop-tutorial
Build a fully functional Web3 shop using Droplinked's APIs — shop API key, product retrieval, cart management, customer addresses, shipping, Stripe checkout, and order retrieval.
In this guide, you'll build your own Web3 shop using Droplinked's API and infrastructure with
only frontend code. By leveraging the ready-made APIs, you can set up a fully decentralized
e-commerce store with minimal backend involvement.
**API reference**
For the full interactive endpoint browser, see the [API Reference](/api-reference/introduction)
tab in this docs site, or the legacy Swagger at
[apiv3.droplinked.com/v1/public-apis/document](https://apiv3.droplinked.com/v1/public-apis/document).
## Setting up — your shop API key (`x-shop-id`)
To make authenticated API calls you first need to generate a unique `x-shop-id` from your
Droplinked dashboard. This ID is included in the headers of every API request to identify and
authorize your shop.
Your shop must be **upgraded** to access API features. API keys are only available to upgraded
shops.
### Steps to generate your `x-shop-id`
Go to [droplinked.com](https://droplinked.com) and sign in.
Navigate to **Account Settings** from the menu.
Open the **Privacy and Security** tab.
In the **Domain** section, enter the domain name from which you plan to make API requests
(e.g. `https://yourstore.com`).
Once the domain is added, your unique shop ID is generated. Copy the `x-shop-id` from the
dashboard — you'll need it for all API calls.
Your request headers should look like this:
```json theme={null}
{
"x-shop-id": "YOUR_SHOP_ID"
}
```
## Base URL and authentication
All Droplinked API requests are made to the following base URL:
```bash theme={null}
https://apiv3.droplinked.com
```
Include your `x-shop-id` in the headers of every API call. This value ties each request to
your specific shop.
```json theme={null}
{
"x-shop-id": "YOUR_SHOP_ID"
}
```
All API requests must include this header. Without it, the request will be rejected.
## API flow overview
The Droplinked API flow follows a simple four-step structure:
Fetch shop information and available products to display in your frontend.
Create a cart, add products, update quantities, and attach customer details (email,
shipping address).
Use the checkout APIs and integrate the Droplinked Stripe payment component to process
customer payments.
Retrieve and display final order details for confirmation or tracking.
## 1. Shop and product retrieval
### Get shop information
Retrieve metadata about your shop — name, description, logo, and other configuration.
* **Endpoint:** `GET https://apiv3.droplinked.com/v1/shop`
* **API docs:** [PublicApiController\_getShop](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_getShop)
### List products (paginated)
Returns a paginated list of products. Use query parameters to control pagination and filtering.
* **Endpoint:** `GET https://apiv3.droplinked.com/v1/products`
* **API docs:** [PublicApiController\_findAll](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_findAll)
### Get product by slug
Useful for SEO-friendly URLs or product pages.
* **Endpoint:** `GET https://apiv3.droplinked.com/v1/products/slug/{slug}`
* **API docs:** [PublicApiController\_getProductBySlug](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_getProductBySlug)
### Get product by ID
Useful for internal lookups or cart operations.
* **Endpoint:** `GET https://apiv3.droplinked.com/v1/products/{id}`
* **API docs:** [PublicApiController\_findOne](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_findOne)
## 2. Cart management
Once a user starts interacting with products, you'll need to create and manage a shopping cart
session.
### Create a new shopping cart
* **Endpoint:** `POST https://apiv3.droplinked.com/v1/cart`
* **API docs:** [PublicApiController\_createCart](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_createCart)
### Retrieve cart information
* **Endpoint:** `GET https://apiv3.droplinked.com/v1/cart/{cartId}`
* **API docs:** [PublicApiController\_getCart](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_getCart)
### Add product to cart
* **Endpoint:** `POST https://apiv3.droplinked.com/v1/cart/{cartId}`
* **API docs:** [PublicApiController\_addProductToCart](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_addProductToCart)
### Update cart item quantity
* **Endpoint:** `PUT https://apiv3.droplinked.com/v1/cart/{cartId}`
* **API docs:** [PublicApiController\_update](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_update)
### Remove item from cart
* **Endpoint:** `DELETE https://apiv3.droplinked.com/v1/cart/{cartId}`
* **API docs:** [PublicApiController\_removeItemFromCart](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_removeItemFromCart)
### Attach additional details to cart
Add customer-specific information such as email, shipping address, and optional notes — usually
required before checkout.
* **Endpoint:** `PATCH https://apiv3.droplinked.com/v1/cart/{cartId}/details`
* **API docs:** [PublicApiController\_attachCartDetails](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_attachCartDetails)
## 3. Customer address (physical products only)
For physical products that require shipping, collect and attach a customer address to the cart.
Address creation is only required if the cart contains physical (non-digital) items.
### Search available countries
* **Endpoint:** `GET https://apiv3.droplinked.com/v1/locations/countries`
* **API docs:** [PublicApiController\_getCountries](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_getCountries)
### Search cities by country
* **Endpoint:** `GET https://apiv3.droplinked.com/v1/locations/cities`
* **API docs:** [PublicApiController\_getCities](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_getCities)
### Create a new customer address
The returned address ID will be included in the **Attach Additional Details to Cart** request.
* **Endpoint:** `POST https://apiv3.droplinked.com/v1/customer/address`
* **API docs:** [PublicApiController\_addAddressToCustomer](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_addAddressToCustomer)
## 4. Shipping management (physical products)
If a cart includes physical products, shipping information must be added before payment.
Droplinked automatically returns available shipping methods as part of the cart data.
### Available shipping options
When you retrieve the cart (`GET /v1/cart/{cartId}`), the response includes a list of available
shipping rates in the `shippingOptions` (or `shipping`) section of the payload. These are
dynamically generated based on:
* The customer's address
* The cart contents (dimensions, weight, etc.)
* Your shop's shipping configuration
Always check for available shipping methods after setting the customer address.
### Add shipping rate to cart
Once you've selected a shipping option, apply it to the cart.
* **Endpoint:** `POST https://apiv3.droplinked.com/v1/checkout/shipping-rates/{cartId}`
* **API docs:** [PublicApiController\_addAnonShippingRate](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_addAnonShippingRate)
Pass the selected shipping-rate ID in the request body.
## 5. Checkout and payment
Once the cart has customer info and (if needed) a shipping rate, kick off the payment process.
Droplinked uses Stripe under the hood and provides a dedicated package to simplify integration
on the frontend.
### Create Stripe payment session
Generate a `clientSecret` for the current cart. This secret is required to initiate the Stripe
payment session on the frontend.
* **Endpoint:** `POST https://apiv3.droplinked.com/v1/checkout/stripe/{cartId}`
* **API docs:** [PublicApiController\_stripeCheckout](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_stripeCheckout)
### Install the Droplinked Stripe UI package
```bash npm theme={null}
npm install droplinked-payment-intent
```
```bash yarn theme={null}
yarn add droplinked-payment-intent
```
```bash pnpm theme={null}
pnpm add droplinked-payment-intent
```
### Implement the payment component
Import and use the `` component. Pass the `clientSecret` you got
from the previous step, along with the callbacks.
```jsx theme={null}
import React from 'react';
import { DroplinkedPaymentIntent } from 'droplinked-payment-intent';
function PaymentPage() {
return (
Complete Your Payment
console.log('Payment successful')}
onCancel={() => console.log('Payment canceled')}
onError={(error) => console.error('Payment failed', error)}
isTestnet={true} // Set to false for production
/>
);
}
export default PaymentPage;
```
Toggle `isTestnet` depending on whether you're using a test or live environment.
## 6. Order retrieval
After a successful payment, retrieve the finalized order using the order ID to show
confirmation details or track status.
### Get order by ID
* **Endpoint:** `GET https://apiv3.droplinked.com/v1/order/{id}`
* **API docs:** [PublicApiController\_publicGetOrder](https://apiv3.droplinked.com/v1/public-apis/document#operation/PublicApiController_publicGetOrder)
## Related
* [API overview](/guides/library/api-overview) — base URL, auth, core concepts
* [API cookbook — build a custom store](/guides/library/api-cookbook) — full reference
implementation in JS + Python
* [Use cases](/guides/library/use-cases) — endpoint-by-endpoint walkthroughs
* [Stripe integration guide](/guides/integrations/stripe) — Connect, webhooks, payouts
# What's new
Source: https://docs.droplinked.com/guides/library/whats-new
Notable product updates and feature releases on Droplinked.
A running log of recent releases on the Droplinked platform — what shipped, what changed, and
what's now available to merchants and developers.
## Affiliate dashboard — redesigned
The Affiliate dashboard has been completely revamped with a fresh, user-friendly design and
improved functionality. It's now easier for producers to manage affiliate settings and for
affiliates to find and promote products.
**Key improvements:**
* **New look** — sleek and intuitive layout for better navigation
* **Simplified process** — easier product setup and promotion
* **Enhanced profit-sharing** — clearer display of earnings and commissions
***
## Revenue sharing
Producers can now easily distribute their earnings across multiple wallets with the new
**Revenue Sharing** feature. Set up multiple wallet addresses, allocate revenue percentages,
and let the system automatically handle the distribution after each sale.
* **Automated distribution** — no manual intervention needed
* **Customizable allocation** — set specific percentages for each wallet
* **Accurate earnings management** — seamless, precise revenue distribution
***
## Invoice creation
Producers can now generate and share customizable invoices directly through Droplinked, making
payment and transaction management smoother than ever.
* **Customizable invoices** — add products, customer details, and shipping info
* **Easy sharing** — share invoices via a unique link
* **Secure payments** — customers can review and pay securely online
***
## Improved dashboard sidebar
The dashboard sidebar UI/UX has been enhanced to be more visually appealing and easier to
navigate. Expect a smoother, more intuitive experience throughout the merchant console.
***
## SKALE network integration (beta)
The SKALE network is now integrated into the platform. Producers can mint products and
customers can make payments on SKALE.
This feature is currently in beta testing.
***
## New product-sharing modal
A new modal makes it easier to use **Product Tile**, **Payment Link**, and **Social Tile**. It
consolidates all tutorials and essential content to simplify sharing your products across
different platforms.
# Order lifecycle
Source: https://docs.droplinked.com/guides/order-lifecycle
The Order V2 flow — from cart initialization through payment intent, distribution calculation, and webhook-driven confirmation.
Every Droplinked order moves through four phases: **init**, **payment intent**, **distribution
calculation**, and **confirmation**. Phases 1–3 are synchronous request/response; phase 4 is
driven by a payment-provider webhook.
## High-level sequence
```mermaid theme={null}
sequenceDiagram
participant Client
participant API as Order API
participant Payment as Payment Provider
participant Webhook as Webhook Handler
Client->>API: 1. Init Order (cartId)
API-->>Client: Returns orderId
Client->>API: 2. Create Payment Intent (orderId, method)
Note over API: 3. Calculate Distribution
API->>Payment: Create Intent
Payment-->>API: clientSecret / checkoutUrl
API-->>Client: Returns Payment Info
Client->>Payment: Complete Payment
Payment->>Webhook: 4. Payment Succeeded Event
Webhook->>API: Confirm Payment (Saga)
Note over API: Process Products, Distribute Revenue
```
## 1. Initialize order
* **Endpoint:** `POST /v2/orders`
* **Input:** `cartId` (UUID)
The API snapshots the current cart state (items, shipping, totals), creates a generic `Order`
record in `PENDING` status, and **locks the cart** to prevent further changes.
**Returns:** `orderId`
## 2. Create payment intent
* **Endpoint:** `POST /v2/orders/:orderId/payment-intent`
* **Input:** `orderId`, `paymentMethod` (e.g. `STRIPE`, `PAYPAL`, `CRYPTO`, `BONUM`, `TELR`)
The API validates the order is pending, runs the distribution calculation (step 3), then
contacts the payment provider to create a payment intent. Distribution metadata (splits +
`orderId`) is attached to the provider's intent so reconciliation can happen later.
**Returns:** `clientSecret` (Stripe) or `checkoutUrl` (PayPal, hosted PSPs).
## 3. Calculate distribution (internal)
Runs synchronously inside step 2. Computes:
* **Droplinked commission** — platform fee
* **Provider fees** — pass-through PSP charges
* **Merchant share** — what settles to the merchant
* **Affiliate / referral splits** — when an attribution session is active
This determines exactly how funds will be split **before** the payment is initialized.
For PSPs that settle off-platform (e.g. Bonum), the saga records the split plan without
executing a live transfer — settlement happens out-of-band and is reconciled later.
## 4. Confirm payment
* **Endpoint:** `POST /v2/orders/:orderId/confirm-payment`
* **Trigger:** Webhook event (e.g. `stripe.payment_intent.succeeded`)
The webhook handler verifies the signature, then executes the **confirmation saga**:
Checks order status; rejects if already terminal.
Routes each line item to the right fulfillment path — POD via [Printful](/guides/integrations/printful),
physical via [EasyPost](/guides/integrations/easypost), and digital items get their
delivery handled inline.
Executes the splits calculated in step 3 (or records them for off-platform settlement).
Updates order status to `CONFIRMED`.
## Idempotency guarantees
Every webhook handler is **idempotent**: replays return 200 without re-processing. State
machines are forward-only — a stale `AUTHORIZED` event arriving after `CAPTURED` is dropped
rather than rewinding the order.
For the full webhook + replay matrix, see [Checkout stability](/guides/testing/checkout-stability).
## Related
* [Payment integrations overview](/guides/integrations/stripe)
* [Checkout stability playbook](/guides/testing/checkout-stability)
# Platform overview
Source: https://docs.droplinked.com/guides/platform/overview
Droplinked is the commerce infrastructure network operating a decentralized inventory management and sales tracking system for digital and physical goods — on-chain registration, authenticated distribution, trustless settlement, and network yield.
**Droplinked streamlines access to capital for SMEs and SMBs by providing asset and collateral
transparency between trade-finance lenders and businesses.** This page explains the platform at
the protocol level — what it is, what problem it solves, and how its four critical functions fit
together.
## The problem
Trade finance is cumbersome, slow, and lacks transparency. Online selling platforms, legacy
payment technology, and third-party intermediaries have taken excess profits across the commerce
supply chain by nature of their centralized infrastructure. Droplinked offers a trustless
solution that cuts through the intermediation by supporting on-chain registration of inventory
alongside an application layer to support interoperability.
## What is Droplinked
Droplinked is the commerce infrastructure network operating a **decentralized inventory
management and sales tracking system** for digital and physical goods.
By recording inventory management on-chain with smart product listings, Droplinked allows
authenticated businesses to easily tie digital or physical products with NFT records for
distribution. Through multiple blockchain integrations, participants automatically and
trustlessly coordinate with financiers, distributors, and publishers to sell items and settle
value. Partnership terms are immutably outlined through their chain of choice to maximize
economic efficiency in a secure and transparent manner for the assets.
Droplinked utilizes stablecoins and tokens to streamline compensation and swap into whatever form
of settlement a party prefers — authenticated producer (manufacturer or company) or publisher
(website, app, or marketplace). The Droplinked YLD token (DYLD) can be staked by contributors to
earn a yield that scales with network growth.
## How it works
The infrastructure combines four critical functions into a single protocol to simplify on-chain
commerce interactions for financiers, producers, and distributors — while incentivizing
contributors through earned ownership in the network. Together, these features streamline
inventory management and sales tracking for merchants and disintermediate legacy affiliate
networks and payment technologies.
Producers register goods and IP on the blockchain of their choice. Each listing and sale is
instantly verifiable on-chain with variables and metadata (color, size, description,
quantity, price, commission).
Producers authorize publishers to distribute their listings under pre-defined SLA contract
terms — enforced by smart contracts with mutual transparency and settlement guarantees.
All sales settle on-chain with immutable splits to producers, owners, distributors,
affiliates, and partners. Every sale takes a **1% protocol fee** into the Droplinked
treasury.
1% of every validated sale accrues to the treasury. Contributors are entitled to yield
through staked DROPS — rewarding early adopters who hold a stake in future growth.
## Decentralized registration of products
Producers register goods and IP across the blockchain of choice. Each listing and sale generated
is instantly verifiable on-chain with variables and metadata associated with the listing —
color, size, description, quantity, price, commission, etc. Once a product's attributes have
been defined, an NFT is minted by SKU or item and paired to on-chain identities for the
responsible parties.
## Authenticated distribution
Producers identify and authorize distributors to distribute and sell their listings, giving
companies the power to authorize partners and pre-define SLA contract terms and affiliate sales.
As these terms are defined through smart contracts on the Droplinked network, companies simplify
their distribution process and rest assured with mutual transparency and settlement guarantees
between parties.
Through authenticated publishers, companies quickly and strategically deploy their listings
across multiple properties while leveraging unique benefits such as **token-gating**, **loyalty
programs**, and custom campaigns and offers.
## Trustless settlement
There are numerous participants across the commerce supply chain. Droplinked simplifies payment
to contributors by recording all sales settlement on-chain, with immutable splits defined to
producers, owners, distributors, affiliates, and partners.
Everyone benefits from transparent settlement with the assurance that payouts are disbursed
based on validated contributions. No longer are there inefficient third-party processes delaying
payment settlement. This resolves fraudulent sales attribution infamously associated with legacy
affiliate infrastructure by leveraging smart product contracts.
With every sale, the Droplinked network takes a **1% protocol fee** which is automatically
deposited into the Droplinked treasury.
## Network yield
1% of the value of every validated sale is automatically taken as a network fee and reserved
into the Droplinked treasury. Additionally, producers and publishers can choose to receive a
portion of their settlement value in **DROPS tokens**. For whatever portion they choose to
receive in DROPS, a proportionate amount of the fiat value of their settlement is also reserved
in the Droplinked treasury.
The treasury grows through the protocol fee as gross network sales increase. Contributors are
entitled to the network yield through staked DROPS. This provides a mechanism for rewarding
early adopters as participants generating a stake in the future growth of the network.
## What's next
Tools, libraries, and endpoints to integrate with the Droplinked protocol — for producers,
publishers, and developers.
Architecture overview of the stack — NestJS backend, React/Next.js frontend, MongoDB, AWS
cloud infra, and lending mechanics.
Headless commerce APIs for stores, products, carts, checkout, and Web3 payments.
# Tech framework
Source: https://docs.droplinked.com/guides/platform/tech-framework
Architecture overview of the Droplinked stack and supporting libraries — NestJS backend, React/Next.js frontend, MongoDB, AWS cloud infrastructure, and the lending logic that combines on-chain inventory records with attested payment history.
The Droplinked technology stack was chosen to align with the goal of delivering a **scalable,
efficient, and user-friendly** platform for both traditional and forward-thinking businesses.
The technical library repository is available at
[github.com/orgs/droplinked/repositories](https://github.com/orgs/droplinked/repositories).
## Backend — NestJS
**NestJS** was selected for the backend due to its robust architecture, which leverages
TypeScript and supports modular development. This allows for highly maintainable and scalable
code.
NestJS also provides built-in support for **GraphQL** and **WebSockets**, essential for
real-time data handling and API management — which is crucial for the project's dynamic nature.
Its strong typing and dependency injection features ensure the codebase remains clean and
manageable, facilitating seamless integration with on-chain technologies and ensuring secure and
efficient handling of blockchain interactions.
## Frontend — React + Next.js
**React** was chosen for the frontend due to its component-based architecture, which promotes
reusability and maintainability. React's virtual DOM ensures high performance by minimizing
direct DOM manipulation, resulting in faster user interface updates. Its extensive ecosystem and
strong community support provide access to a wide range of libraries and tools, enabling rapid
development and iteration. React's declarative nature simplifies the development process,
allowing us to create a dynamic and responsive user experience aligned with the goal of
delivering an intuitive and engaging platform.
**Next.js** powers the custom shopfront for SEO-friendliness and server-side rendering of
storefront pages.
## Data layer
Primary application database — cost-efficient, reliable, and well-suited to the dynamic
schema of products, listings, and orders.
Object storage for product images, banners, exports, and other binary assets.
## Cloud infrastructure (AWS)
Droplinked runs on AWS using the services that map naturally to its commerce + AI workloads.
| Service | Used for |
| ---------- | ------------------------------------------------------------- |
| ECS | Containerized service orchestration (backend, 3rdp, services) |
| ECR | Image registry for the service containers |
| CloudFront | CDN for storefronts and static assets |
| S3 | Object storage |
| EC2 | Long-running compute |
| ELB | Load balancing |
| SageMaker | ML model training and hosting |
| Bedrock | Managed foundation models (image, text, NSFW classification) |
| Lambda | Event-driven serverless workloads |
## Lending logic and mechanics
Droplinked combines **on-chain inventory records** with **attested payment-history data** for
loan origination. This is the foundation of the dual-token framework — Lend DROPS (DLND) for
senior secured exposure to real-world trade finance, with Yield DROPS (DYLD) as junior
risk-buffer capital.
Combining on-chain inventory records with attested payment history data — borrowers are
underwriten using verifiable network activity rather than only off-chain bank statements.
Compared to traditional origination lending, the on-chain flow compresses the parties
involved and the steps needed to move from application to funded principal.
The framework supports automated commercial filings based on geography and jurisdiction
for liabilities — so the legal wrapper around each loan pool stays in sync with the
on-chain state.
## What's next
The producer / publisher / lender model and how Droplinked composes commerce, identity, and
on-chain settlement.
Tools, libraries, and endpoints to integrate with the Droplinked protocol.
Programmatic access to stores, products, carts, and Web3 payments.
MCP server + ACP feed for agentic discovery and checkout against Droplinked merchants.
# Droplinked toolbox
Source: https://docs.droplinked.com/guides/platform/toolbox
Tools and endpoints to unlock the power of Web3 commerce as a producer or publisher — developer sandbox libraries, OAuth, DIMST Console, embeddable cards, MetaMask Snap, and the testnet roadmap.
Learn how to use Droplinked tools and endpoints to unlock the power of Web3 commerce as a
producer or publisher.
This guide provides documentation for the tools necessary to interact with the Droplinked
protocol. It is geared towards users, system administrators, and technical resources wishing to
understand and integrate with the protocol.
## Droplinked developer sandbox
There are various libraries that provide the base infrastructure required for Droplinked's
operation on any given chain.
Fractionalize product NFTs so each fraction represents one purchasable unit on a chain of
your choice.
Full suite of Web2 and Web3 enabled functionalities for headless, no-code, and low-code
environments — AI agents, software platforms, and developers.
Decentralized Inventory Management and Sales Tracking registry available per chain of
choice.
Proof of Attendance (POAP) for on-chain ticket and event management.
### DIMST registry by chain
Open-source contracts implementing the Decentralized Inventory Management and Sales Tracking
registry across supported chains.
| Chain | Source |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Stacks | [gitlab.com/flatlay/droplinked-contract](https://gitlab.com/flatlay/droplinked-contract) |
| NEAR | [github.com/FLATLAY/Droplinked-NEAR-Contract](https://github.com/FLATLAY/Droplinked-NEAR-Contract/blob/main/README.md) |
| SKALE | [github.com/FLATLAY/droplinked\_skale](https://github.com/FLATLAY/droplinked_skale) |
| XRPL (EVM side-chain) | [github.com/FLATLAY/droplinked-ripple](https://github.com/FLATLAY/droplinked-ripple/blob/main/README.md) |
| Hedera | Supported |
| Base | Supported |
| Polygon | Supported |
| Solana | Supported |
| Bitlayer | Supported |
| Redbelly Network | Supported |
## Interacting with the protocol
To interact with the Droplinked protocol, Droplinked provides a set of key tools.
### OAuth
Use crypto-wallet credentials to authenticate and interact with the Droplinked protocol. Wallet
OAuth removes friction for both merchants and customers and keeps the authorization surface
entirely Web3-native.
### DIMST Console
CLI and UI for simple execution of product registration, inventory management, sales tracking,
and batch operations.
### Embeddable cards
Headless embeddable units for inventory display that allow producers and publishers to quickly
and easily embed Droplinked-registered inventory onto their own pages, properties, and sites
with an iFrame component. The cards operate completely decentralized — or with minimal
centralized functionalities for Web2/Web3 enablement.
### MetaMask Snap
A direct integration with the native MetaMask userbase to interact with the protocol — create
decentralized shopfronts, product drops, and checkout flows.
## Timeline
Deployment across the supported networks allows for commerce settlement in the native token for
each respective network and will serve as the foundation for cross-chain interoperability
bridges to Droplinked's protocol.
Droplinked testnet development is underway. The testnet will tie together the base
infrastructure that allows for a holistic cross-chain protocol — complete with token
contracts, the Droplinked treasury, and staking functionality.
## What's next
Architecture overview of the stack — backend, frontend, infrastructure, and lending
mechanics.
Programmatic access to the protocol — stores, products, carts, checkout.
MCP server + ACP feed for agentic discovery and checkout against Droplinked merchants.
# Supply-chain integrity
Source: https://docs.droplinked.com/guides/security/supply-chain
How the Droplinked backend build verifies node_modules and pins native binaries to defend against tampered packages.
This document records the supply-chain hardening posture for the `droplinked-backend` build
pipeline. It pairs three things: `package.json` `overrides`, an `npm audit signatures` step
in the Docker build, and exact-version pinning of native binaries.
## Threat model
A malicious replacement of a native binary inside `node_modules/` loads at app startup with
**full process privileges** — direct access to `process.env` (JWT secret, database URL,
Stripe / AWS / Atlas credentials) and the EC2 instance-metadata endpoint.
The `sharp` package ships its native image-processing binary under `@img/sharp-*` and
`@img/sharp-libvips-*`. It's the most attractive target in this repo's dependency graph
because it loads on every request that touches the image pipeline.
## Two layers of defense
### 1. Exact-version pinning of `sharp` native binaries
`package.json` `overrides` pins the `@img/sharp-*` binaries that actually load at runtime
on ECS containers (Alpine / musl, x64) **and** the glibc x64 variant in case the base image
moves:
| Package | Pinned version |
| ---------------------------------- | -------------- |
| `@img/sharp-linux-x64` | `0.33.5` |
| `@img/sharp-linuxmusl-x64` | `0.33.5` |
| `@img/sharp-libvips-linux-x64` | `1.0.4` |
| `@img/sharp-libvips-linuxmusl-x64` | `1.0.4` |
These versions match the `sharp@^0.33.5` release. The integrity `sha512` hashes for each
tarball are stored in `package-lock.json` and are **verified by npm on every install**.
To check the upstream tarball hashes manually:
```bash theme={null}
npm view @img/sharp-linuxmusl-x64@0.33.5 dist
npm view @img/sharp-libvips-linuxmusl-x64@1.0.4 dist
```
The `shasum` returned by `npm view` corresponds to the sha1 of the tarball; the lockfile
records the sha512. Compare against the release notes at
[sharp releases](https://github.com/lovell/sharp/releases).
### 2. `npm audit signatures` at build time
The `Dockerfile` runs `npm audit signatures` immediately after `npm i`. This walks every
installed package and verifies its registry-issued signature against
[npm's signing key](https://registry.npmjs.org/-/npm/v1/keys).
The build **fails** on any package whose signature is missing or doesn't match — including
binaries swapped in by a compromised post-install script or a tampered registry mirror.
## Runbook
### When `npm audit signatures` fails in CI
A signature failure means at least one tarball we resolved doesn't match what npm
signed.
The error names the offending package(s) and version(s).
Has it been unpublished or re-published? Has the publisher account been compromised
(check the npm status feed)?
If the failure is on a transitive dependency we don't directly own, pin the parent
dependency to a known-good version via `overrides` and regenerate the lockfile.
Do **not** bump the pin until the root cause is understood. File an incident.
### Rotating sharp to a new patch version
When [`lovell/sharp`](https://github.com/lovell/sharp/releases) ships a new `0.33.x`
release:
Confirm there is no breaking change to the API surface used by the image-pipeline
consumers.
Update the `@img/sharp-*` pins in `overrides` to the new exact versions (plus any new
`@img/sharp-libvips-*` versions noted in the sharp release).
`npm install --package-lock-only`. Confirm the `integrity` field for each `@img/sharp-*`
entry changed.
`docker build -t supply-chain-test .` — confirm the `npm audit signatures` step passes.
Reviewer cross-checks the upstream release.
### Rotating to a major sharp version (0.33 → 0.34)
Same as above plus:
* Re-baseline this document
* Audit every call site in the image pipeline for breaking API changes
* Run the image-pipeline regression suite before merge
## Related
* [Deploy flow](/guides/deploy-flow) — how a verified build reaches dev/live.
* [Environment variables](/guides/environment-variables) — the secrets the threat model
protects.
# Checkout stability playbook
Source: https://docs.droplinked.com/guides/testing/checkout-stability
Enterprise-grade test sequences for the customer checkout flow — fixtures, smoke matrix, cross-PSP coverage, idempotency, and chaos drills.
This playbook is the minimum required test matrix before any change to cart, shipping,
payment, or order modules merges to `main`. It pairs backend orchestration, the payment
gateway, the checkout UI, and the storefront — a missed regression in any of these costs
real orders.
## Cadence
* **Per PR** touching cart, order, payments, or any PSP module: run the smoke sequence
* **Weekly** against production: full matrix (smoke + cross-PSP + idempotency)
* **Pre-release** for every backend tag: full matrix + chaos drills
## 1. Test fixtures
A reliable stability program needs known-good and known-broken fixtures so every regression
test has predictable inputs.
Never point smoke tests at real merchant shops. Use the fixture shops below.
### Fixture shops
| Slug | Purpose | Shipping profile | Payment methods |
| ---------------------- | ------------------------------------------ | --------------------------- | ------------------------------------------------ |
| `qa-physical-easypost` | Real EasyPost rates, physical goods, US→US | EasyPost | Stripe (test), PayMob (sandbox), Bonum (sandbox) |
| `qa-physical-printful` | POD goods via Printful | Printful | Stripe (test) |
| `qa-physical-uae` | UAE→MENA, exercises Telr + PayMob | Custom flat-rate by country | Telr (sandbox), PayMob (sandbox), Stripe (test) |
| `qa-digital-only` | Pure digital items, no shipping | none | Stripe (test), Bonum (sandbox) |
| `qa-mixed` | Physical + digital, split shipping | EasyPost (physical only) | Stripe (test) |
| `qa-crypto` | Web3 product, on-chain settlement | none / digital | x402 / Coinbase Commerce / SOL Pay |
### Fixture customer accounts
Test-mode-only — never real cards.
| Email | Card | Address country | Why |
| ---------------------------- | --------------------- | --------------- | ----------------------------------- |
| `qa+stripe-pass@...` | `4242 4242 4242 4242` | US | Happy path |
| `qa+stripe-3ds@...` | `4000 0025 0000 3155` | US | Forces 3DS challenge |
| `qa+stripe-decline@...` | `4000 0000 0000 0002` | US | Generic decline |
| `qa+stripe-insufficient@...` | `4000 0000 0000 9995` | US | Insufficient funds |
| `qa+stripe-disputed@...` | `4000 0000 0000 0259` | US | Triggers dispute post-charge |
| `qa+stripe-mena@...` | `4242 4242 4242 4242` | AE / SA / EG | AVS variations, currency conversion |
### Fixture environments
* **Stage** (`apiv3stage` + `checkoutstage`) — primary smoke target. Hits Stripe **test mode**,
EasyPost test API, PayMob sandbox, Telr sandbox.
* **Production** (`apiv3prod` + `checkout.droplinked.io`) — full live target. Run from a
UAE-IP runner for IP-allowlist coverage.
* **Local docker compose** — backend-only assertions; can't exercise full Stripe Elements.
### Fixture freshness
Schedule a weekly clean of `qa-*` shop carts older than 7 days, void all `qa+*` test
charges, and archive completed test orders. Stops fixture rot from masking regressions.
## 2. Per-PR smoke sequence
Twelve assertions. Should complete in under 10 minutes against stage.
| # | Scenario | Fixture | Expected |
| ------- | --------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------- |
| **S1** | Add 1 physical item → checkout init | `qa-physical-easypost` | 200, cart ID returned, totals match items + 0 shipping |
| **S2** | Enter US shipping address → `availableShipping` populated | + US addr | ≥1 shipping group, ≥1 rate per group with `rateId` + price + carrier |
| **S3** | Select shipping rate → totals update | continued | `totals.shipping` reflects rate; `totals.total = items + shipping + tax` |
| **S4** | Create Stripe PaymentIntent → `client_secret` | continued | `clientSecret` + `paymentIntentId` present, no shipping mismatch |
| **S5** | Pay with `4242…` → order moves to `PAID` | continued | Order flips `PENDING`→`PAID` within 30s of webhook |
| **S6** | Stripe charge has populated branding fields | Stripe dashboard test mode | `statement_descriptor_suffix`, `receipt_email`, `shipping` block populated |
| **S7** | Digital-only cart skips shipping entirely | `qa-digital-only` | Frontend hides shipping section; checkout completes with billing only |
| **S8** | Mixed cart splits shipping correctly | `qa-mixed` | Only physical item in `availableShipping`; digital is unshippable but non-blocking |
| **S9** | EasyPost-tagged product gets real carrier rates | `qa-physical-easypost`, US→US | USPS / UPS / FedEx appear; no silent fallthrough to custom strategy |
| **S10** | Printful-tagged product gets Printful rates | `qa-physical-printful` | Printful strategy fires; no EasyPost call |
| **S11** | 3DS challenge card completes payment | `qa+stripe-3ds@…` | Redirects to 3DS challenge, returns, charge completes |
| **S12** | Decline card lands clean error | `qa+stripe-decline@…` | Generic "card declined" surfaced, cart preserved, no order created |
A PR that breaks any of S1–S12 against stage must not merge.
S1–S6 + S7 + S9 should run automatically in CI. S5, S11, S12 require Stripe sandbox client
interaction (Playwright against test mode).
## 3. Cross-PSP matrix (weekly)
Run the same 12 scenarios across each enabled PSP for the relevant currency.
| PSP | Currency | Origin | Dest |
| ----------------- | -------- | ------ | ---------------------------- |
| Stripe | USD | US | US |
| Stripe | AED | AE | AE |
| Stripe | AED | AE | SA |
| PayMob | EGP | EG | EG |
| PayMob | AED | AE | AE |
| Telr | AED | AE | AE |
| Bonum | MNT | MN | MN |
| Coinbase Commerce | USDC | global | digital (digital-only carts) |
| x402 / SOL Pay | USDC | global | digital (digital-only carts) |
| PayPal | USD | US | US |
For each row, capture:
* Time-to-complete checkout (p50, p95)
* Webhook latency (PSP → integration service → backend)
* Order status transition path
* Cart-preserved-on-failure behavior
## 4. Idempotency + replay matrix
Every PR that touches webhook handlers or cart-status transitions must pass:
| # | Scenario | Expected |
| ------ | ------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| **I1** | Same webhook delivered twice (Stripe replay) | Second is no-op; no double-paid; no duplicate emails |
| **I2** | Webhook arrives before payment-intent succeeded callback | Backend resolves to correct final state regardless of arrival order |
| **I3** | Webhook arrives 24h late (Stripe retries) | Still processed correctly; idempotency key honored |
| **I4** | Two concurrent "Pay Now" clicks | One PaymentIntent created; second click 4xxs cleanly (no double-charge) |
| **I5** | User abandons checkout, returns next day | Cart preserved if not expired; expired carts show empty-state, not 500 |
| **I6** | Selected shipping rate expires (EasyPost \~15min TTL) | Backend revalidates; customer asked to reselect; no silent total drift |
| **I7** | Network failure mid-payment (Stripe `requires_action` → connection drop) | Order reconciles via webhook within 5 minutes |
## 5. Chaos drills (pre-release / monthly)
Test failure modes that only happen in production.
| # | Drill | How to simulate | Pass criteria |
| ------ | ------------------------------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **C1** | EasyPost insufficient funds | Drain EasyPost wallet in sandbox | Backend alerts ops; existing checkouts fall back to flat-rate; new checkouts surface a graceful banner |
| **C2** | Stripe 5xx | Toxiproxy outage simulation | Backend retries once, then fails gracefully; cart preserved |
| **C3** | Mongo Atlas connection drop | Block Atlas IP in test env | Backend serves cached cart; surfaces error on writes; no 500s to reads |
| **C4** | Integration service down | Stop the 3rdp container | Backend opens circuit breaker after 3 failures; surfaces "Payment provider unavailable" within 2s |
| **C5** | Webhook signature failure (rotated key) | Send webhook with stale HMAC | 401, log, alert; do NOT process the event |
| **C6** | Restricted Stripe key from unauthorized IP | Spin up runner without IP allowlist | Stripe returns 403; backend should not retry-loop |
| **C7** | UAE→US Stripe currency conversion | AED-priced order, US card | Conversion happens once; receipt + dashboard agree on FX rate |
| **C8** | Mixed cart with one out-of-stock item | Stock one item to 0 mid-checkout | Item removed with clear message; cart total recomputes; selected shipping rate revalidates |
## 6. Known regression — EasyPost silently uses custom strategy
A product configured with `shippingProfileId = 'easypost'` can silently fall through to the
custom shipping strategy if the strategy factory only registers Printful + Custom. The
custom strategy's `canHandle` returns true for any non-Printful profile, so EasyPost-tagged
products get routed to a strategy expecting merchant flat-rate config. Result: empty
`availableShipping`, checkout dead-ends at "Shipping these items is not available to this
address."
**Fix path:**
Wrap the `easy-post` module's rate-fetch in a `ShippingRateStrategy` adapter.
Add it to the strategy factory's default-initialization list before the custom strategy.
Update the custom strategy to only return `true` for explicit `'custom'` / `'flat-rate'`
identifiers — never as a catch-all.
S9 in the per-PR smoke matrix is the standing regression guard once the strategy ships.
## 7. Tooling roadmap
What we have:
* A narrowly-Bonum stress-test script — pattern-extend for other PSPs
* Some payment-gateway specs needing repo-level jest cleanup
* A backend CI workflow with lint+test as a soft-fail
What we need:
Drive Stripe Elements test-mode card entry; assert on backend cart state via direct
API checks between UI steps.
`checkout-smoke.yml` runs the Playwright suite on every PR labelled `area:checkout` or
touching cart/order/payments modules.
Isolate regressions from real-money flow with a dedicated dashboard tag.
Run sections 2 + 3 + 4 against production weekly from a UAE-IP runner; post to a
`#stability` channel.
Toxiproxy (or similar) for section 5 drills.
## Appendix — "Shipping these items is not available" fault tree
```
"Shipping these items is not available to this address"
└── frontend: MethodsLoader status === 'failed'
└── ShippingMethodsSection: noShippingAvailable === true
└── cart.availableShipping has length 0
├── FetchShippingRateUseCase returned hasErrors: true
│ ├── No strategy matched profileId
│ ├── Strategy threw (EasyPost outage, Printful 5xx)
│ └── Address mapping failed (country not in DB)
└── FetchShippingRateUseCase returned empty responses
├── Custom strategy: shop has no flat-rate config for destination
├── Printful strategy: API returned no shippable carriers
└── EasyPost insufficient funds (manifests as no rates)
```
Walk this tree from leaf upward to pin which node fired. Update the tree when a new node
is discovered.
## Related
* [Order lifecycle](/guides/order-lifecycle)
* [Stripe](/guides/integrations/stripe), [PayPal](/guides/integrations/paypal),
[Bonum](/guides/integrations/bonum), [Telr](/guides/integrations/telr)
* [EasyPost](/guides/integrations/easypost), [Printful](/guides/integrations/printful)
# Brand attestation: request → mint → verify
Source: https://docs.droplinked.com/guides/trust-fabric/brand-attestation-lifecycle
End-to-end walkthrough of the Schema A brand-attestation lifecycle for merchants, AI agents, and third-party verifiers.
A Schema A `BrandAttestation` is the on-chain receipt that proves a brand slug
on droplinked has been operator-reviewed and is who it says it is. This page
walks the full lifecycle — request → operator approval → EAS Schema A mint →
merchant polling → public verifier read — across the three audiences that
touch it: merchants requesting from the shop builder, AI agents requesting on
behalf of a merchant via MCP, and third-party verifiers reading the resulting
on-chain attestation.
Schema A is currently anchored on **Base Sepolia testnet**. Mainnet flip is
gated on the KMS-backed signer migration. Every endpoint below returns the
active chain in its response so consumers can verify on the right
[easscan domain](https://easscan.org).
## When to use this guide
* **Merchants** — you clicked **Request brand attestation** in the shop
builder and want to know what happens next.
* **AI agents** — you're orchestrating a merchant-onboarding flow via the
droplinked MCP server and need to drive the same lifecycle programmatically.
* **Third-party verifiers** (lenders, partners, regulators) — you need to
resolve a brand attestation UID to chain-anchored facts without trusting
droplinked.
If you only want the API contract for the two endpoints, jump straight to the
[Brand attestation request reference](/api-reference/public/brand-attestation-request).
If you want the architectural context for Schema A in the 4-axis trust fabric,
see [Trust Fabric overview](/concepts/trust-fabric).
## The state machine
A brand-attestation request walks five discriminator values. Four are real
queue-row states; the fifth (`NONE`) is a synthetic state the status endpoint
returns when no row exists yet, so the widget can render the initial CTA
without 404-handling.
No row in the queue for this `shopSlug`. The status endpoint returns
`{ "status": "NONE", "request": null }` and always 200. UIs render the
**Request brand attestation** CTA in this state.
Merchant has clicked the CTA. A queue row is in
SUPER\_ADMIN review. Re-clicking the CTA is idempotent — the API returns
the same `requestId` instead of duplicating the row. UIs render
"Pending operator review — we'll email you when it ships".
Operator has approved the row. The mint orchestrator will attempt a
Schema A on-chain mint. If the mint fails (RPC drop, gas spike,
wallet-nonce contention) the row **stays APPROVED**; `mintError` +
`mintAttempts` persist on the operator-side row for retry. The row is
**never auto-flipped back** to PENDING or REJECTED.
Schema A attestation is on-chain. `attestationUid` + `mintedAt` are
populated on the public projection. The widget can quote the easscan
reference without a second round-trip. This is a terminal state — the
row is never re-minted.
Operator declined the row with a reason (visible only on the
SUPER\_ADMIN console — never on the public projection). Terminal state.
The merchant may submit a fresh `PENDING` row at any time; the
partial-unique index on `(shopSlug, status: PENDING)` does NOT block
re-submits after a terminal state.
## Step 1 — Request the attestation
There are three equivalent paths to queue a request. They all hit the same
backend endpoint and the same idempotency check.
From your merchant dashboard, open the **Trust Fabric** card and click
**Request brand attestation**. Optionally add a free-text note (max
2048 chars — surfaces only to the operator). The card immediately
flips to "Pending operator review".
No further action is required from the merchant. The status card polls
`GET /v2/attestations/brand/:shopSlug/request-status` every 60s and
auto-advances through `PENDING → APPROVED → MINTED` (or `→ REJECTED`).
An AI agent orchestrating onboarding calls the MCP tool wired against
the droplinked MCP server.
```typescript theme={null}
const result = await mcp.callTool('request_brand_attestation', {
shopSlug: 'unstoppable',
notes: "Onboarding push initiated by agent on 2026-06-13",
});
```
```json theme={null}
{
"requestId": "65f8a1b2c3d4e5f6a7b8c9aa",
"status": "PENDING",
"message": "Your request is in the operator review queue"
}
```
See [Brand attestation MCP tools](/agentic/lender-trinity-mcp-tools#request_brand_attestation)
for the full envelope.
If you're driving the flow from your own backend or a third-party
portal, hit the endpoint directly. No JWT, no IP-allowlist — the
endpoint is `@Public()`.
```bash theme={null}
curl -X POST https://apiv3.droplinked.com/v2/attestations/brand/unstoppable/request \
-H 'content-type: application/json' \
-d '{ "notes": "Q3 partner pitch on 2026-07-15" }'
```
```json theme={null}
{
"requestId": "65f8a1b2c3d4e5f6a7b8c9aa",
"status": "PENDING",
"message": "Your request is in the operator review queue"
}
```
Idempotent on `(shopSlug, status: PENDING)`. Re-submitting while a
`PENDING` row exists returns the same `requestId` — never a 4xx.
**Fail-open by design.** The endpoint hard-casts the response `status` to
`PENDING` regardless of whether the row is new or re-found by the
idempotency check. UIs should never have to disambiguate "first submit" vs
"re-submit" — the discriminator alone drives the success toast.
## Step 2 — Poll the status
Once a request is queued, walk the discriminator with the status endpoint.
The widget polls this; an agent can long-poll it; a third-party portal can
display it.
```bash theme={null}
curl -s https://apiv3.droplinked.com/v2/attestations/brand/unstoppable/request-status | jq .
```
The endpoint **always** returns 200 — even when no row exists. Resolution
rule: any `PENDING` row wins (there is at most one per slug by the partial
index); otherwise the newest terminal row (`createdAt` desc) is returned.
### Response shapes per state
```json theme={null}
{
"status": "NONE",
"request": null
}
```
No row in the queue. Render the initial CTA.
```json theme={null}
{
"status": "PENDING",
"request": {
"requestId": "65f8a1b2c3d4e5f6a7b8c9aa",
"shopSlug": "unstoppable",
"status": "PENDING",
"attestationUid": null,
"mintedAt": null,
"createdAt": "2026-06-12T18:00:00Z"
}
}
```
Awaiting operator review. `attestationUid` + `mintedAt` are `null`.
```json theme={null}
{
"status": "APPROVED",
"request": {
"requestId": "65f8a1b2c3d4e5f6a7b8c9aa",
"shopSlug": "unstoppable",
"status": "APPROVED",
"attestationUid": null,
"mintedAt": null,
"createdAt": "2026-06-12T18:00:00Z"
}
}
```
Operator approved; the mint orchestrator is engaged. The row stays
here on mint failure — operator-side `mintError` + `mintAttempts`
drive the retry, never surfaced on the public projection.
```json theme={null}
{
"status": "MINTED",
"request": {
"requestId": "65f8a1b2c3d4e5f6a7b8c9aa",
"shopSlug": "unstoppable",
"status": "MINTED",
"attestationUid": "0x9c4f7a3e8b1d2c6f5a0b8e9d1c2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f",
"mintedAt": "2026-06-13T07:42:11Z",
"createdAt": "2026-06-12T18:00:00Z"
}
}
```
Terminal state. `attestationUid` is the EAS UID — see Step 3 for the
verifier link.
```json theme={null}
{
"status": "REJECTED",
"request": {
"requestId": "65f8a1b2c3d4e5f6a7b8c9aa",
"shopSlug": "unstoppable",
"status": "REJECTED",
"attestationUid": null,
"mintedAt": null,
"createdAt": "2026-06-12T18:00:00Z"
}
}
```
Terminal state. The operator-side `decisionReason` is **scrubbed** from
the public projection — merchants will see the reason via the
operator-side notification (email), not via this endpoint. The
merchant may re-submit a fresh request at any time.
**Operator-private fields stay private.** `decidedBy`, `decisionReason`,
`mintError`, `mintAttempts`, `merchantId`, and the merchant's free-text
`notes` are scrubbed from the public projection. They only surface on the
SUPER\_ADMIN admin route. The shape above is the full public surface.
### Polling cadence
* **Merchant widget**: 60s. The state walks `PENDING → APPROVED → MINTED`
over operator-review timelines (minutes to hours); a 60s poll cadence
feels live to the merchant without hammering the endpoint.
* **AI agent**: prefer event-driven UX. If you can park the conversation
and ping the merchant when state advances, do that. Otherwise long-poll
at 30s intervals for up to 5 minutes — beyond that, hand the lifecycle
back to the merchant.
* **Third-party verifier**: don't poll. Read once at decision time and
cache the `attestationUid` — the EAS attestation is immutable once
minted.
## Step 3 — See the mint land on-chain
Once `status === "MINTED"`, the `attestationUid` resolves to a Schema A
attestation on the Ethereum Attestation Service. The easscan link scheme:
```
https://base-sepolia.easscan.org/attestation/view/
```
For example (placeholder UID — your real UID will be different):
```
https://base-sepolia.easscan.org/attestation/view/0x9c4f7a3e8b1d2c6f5a0b8e9d1c2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f
```
The UID above is **example-only** — copy-pasting it will resolve to a
"not found" page on easscan. Use the real `attestationUid` returned by
your `request-status` response.
The easscan UI surfaces:
* The Schema A definition (the `BrandAttestation` schema UID)
* The **issuer wallet** — droplinked's operator signer
* The **recipient** — the merchant's on-chain address
* The decoded payload — `brandSlug`, `displayName`, `issuedAt`,
`expiresAt`, and the registry pointers
* A timeline of any subsequent revocations (Schema A revocations are rare
and operator-driven; the reconciler never auto-revokes)
For the public verifier read **without** going through easscan, hit:
```bash theme={null}
curl -s https://apiv3.droplinked.com/v2/attestations/brand/unstoppable | jq .
```
This returns the same chain-anchored fields plus the active-chain marker so
your client picks the right easscan domain for hyperlinks.
## Step 4 — For AI agents
Two MCP tools cover the full agent-side lifecycle, both published by the
droplinked MCP server (`mcp.droplinked.com`):
### `request_brand_attestation`
```typescript theme={null}
const result = await mcp.callTool('request_brand_attestation', {
shopSlug: 'unstoppable',
notes: 'Filed on behalf of merchant during agentic onboarding',
});
```
```json theme={null}
{
"requestId": "65f8a1b2c3d4e5f6a7b8c9aa",
"status": "PENDING",
"message": "Your request is in the operator review queue"
}
```
Use this when an agent is orchestrating a merchant's full onboarding
flow — application form, lender match, brand-attestation request — and
needs to queue the request without the merchant manually clicking the
shop-builder CTA. The merchant must still own the shop slug; the tool
does not bypass the operator review gate.
### `get_brand_attestation_status`
```typescript theme={null}
const status = await mcp.callTool('get_brand_attestation_status', {
shopSlug: 'unstoppable',
});
```
```json theme={null}
{
"status": "MINTED",
"request": {
"requestId": "65f8a1b2c3d4e5f6a7b8c9aa",
"shopSlug": "unstoppable",
"status": "MINTED",
"attestationUid": "0x9c4f7a3e8b1d2c6f5a0b8e9d1c2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f",
"mintedAt": "2026-06-13T07:42:11Z",
"createdAt": "2026-06-12T18:00:00Z"
}
}
```
Use this in an agent loop to detect terminal state (`MINTED` or
`REJECTED`) before handing back to a human. The five-state discriminator
maps cleanly to a switch statement — same shape as the public HTTP
endpoint, no envelope translation.
A typical agent sequence:
1. Call `request_brand_attestation` once at onboarding time.
2. Call `get_brand_attestation_status` on a backoff (30s → 60s → 120s)
until `status` is `MINTED` or `REJECTED`.
3. On `MINTED`, call `verify_brand_attestation` (Schema A read) to
confirm the on-chain envelope decodes cleanly. Then hand the merchant
the easscan link from Step 3.
See [Lender Trinity MCP Tools](/agentic/lender-trinity-mcp-tools) for
the full catalog including these two tools.
## Step 5 — For third-party verifiers
A third-party verifier (lender, partner, regulator) does **not** poll the
request lifecycle. They consume the terminal artifact: the on-chain Schema
A attestation referenced by `attestationUid`.
The two canonical reads:
### easscan UI (human verifier)
Open `https://base-sepolia.easscan.org/attestation/view/` in a
browser. Decoded payload + issuer wallet + timeline are visible without
running any infrastructure.
### Droplinked public read (programmatic verifier)
```bash theme={null}
curl -s https://apiv3.droplinked.com/v2/attestations/brand/ | jq .
```
Returns the chain-anchored claim envelope with the active-chain marker.
Pair it with the public lender-registry endpoint to walk the trust trinity
(brand → registered lender → underwritten credit-risk):
```bash theme={null}
# 1. Brand attestation
curl -s https://apiv3.droplinked.com/v2/attestations/brand/unstoppable | jq .
# 2. Per-lender lookup if you need to verify a credit-risk attestation issuer
curl -s https://apiv3.droplinked.com/v2/lenders/crediblex-uae | jq .
# 3. Underwriting signals composite (Schema B + C rollup)
curl -s https://apiv3.droplinked.com/v2/underwriting-signals/ | jq .
```
See [Lender Registry Lookup](/api-reference/public/lender-registry) +
[Underwriting Signals](/api-reference/public/underwriting-signals) for
sibling-axis reads.
Public verifier reads return only chain-anchored claims + aggregate
identifiers — never the operator-side `notes`, `decisionReason`,
`decidedBy`, or PII. Verifier-side policy decides whether the chain
artifact is sufficient for the verifier's risk threshold; droplinked
never blocks reads on its own opinion of "good standing".
## Common errors
| Symptom | Cause | Fix |
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST` returns `404` | `shopSlug` not found via `ShopService` | Confirm the slug matches the kebab-case URL slug, not the human-display name. |
| `POST` returns `400` "notes too long" | `notes` exceeded 2048 chars | Trim the note; only the operator sees it anyway. |
| `GET request-status` returns `NONE` after a successful `POST` | You're hitting a stale CDN cache (rare) or the wrong slug | Confirm the slug. The endpoints are not edge-cached at the application layer. |
| Status stuck at `APPROVED` for >24h | Mint orchestrator hit a persistent RPC error | The row is recoverable — operator-side `mintError` + `mintAttempts` drive the retry. Email `support@droplinked.com` with the `requestId` if it persists. |
| `MINTED` with a UID that doesn't resolve on easscan | Wrong chain — you're on the wrong easscan domain | Use `base-sepolia.easscan.org` (testnet) until mainnet flip. The active chain is returned in the chain-anchored read at `/v2/attestations/brand/:slug`. |
| Repeated `POST` returns the same `requestId` | This is **expected** — the endpoint is idempotent on `(shopSlug, status: PENDING)` | Treat as success. |
## Related
The 4-axis architecture context for Schema A and its sibling schemas.
Full catalog of MCP tools including `request_brand_attestation` and `get_brand_attestation_status`.
Endpoint-level reference for `POST /request` + `GET /request-status`.
Drop-in widget for surfacing brand-attestation status on your own dashboard.
# Introduction
Source: https://docs.droplinked.com/introduction
Build commerce — and agentic commerce — on Droplinked.
Droplinked is commerce infrastructure. Merchants publish inventory, customers buy it on
storefronts, and AI agents increasingly discover and purchase it on the buyer's behalf. These
docs are the single source of truth for building on the platform — the REST API, the agentic
commerce layer (MCP + Stripe ACP), and the concepts that tie them together.
Make your first API call against the public catalog in under five minutes.
Public endpoints, merchant JWTs, and integration keys.
The full REST surface — shops, products, carts, orders, and more.
Make a merchant's inventory shoppable by AI agents via MCP + the ACP feed.
## What you can build
* **Storefronts & commerce** — read public catalogs, build carts, run checkout, place orders
across physical, digital, and print-on-demand inventory.
* **Merchant tooling** — manage shops, products, collections, pricing, and shipping profiles.
* **Payments** — accept card (Stripe), PayPal, regional PSPs, and **crypto/stablecoin** settlement,
turnkey per the shop's configured payment methods.
* **Agentic distribution** — project a merchant's existing inventory into AI agent surfaces
(ChatGPT, Claude, Cursor) with the MCP server, the Stripe ACP feed, and on-chain affiliate splits.
## Where to start
New to the platform? Read the [platform model](/concepts/platform-model) for the core objects
and surfaces, then run the [quickstart](/quickstart). Building an agent integration? Jump to
[Agentic Commerce](/agentic/overview).
# Quickstart
Source: https://docs.droplinked.com/quickstart
Make your first Droplinked API calls in under five minutes.
This walks you through reading a merchant's public catalog — no credentials required — then
points you at the authenticated and agentic paths.
## 1. Pick an environment
Use **production** for live data or **development** for a sanitized sandbox. See
[Environments](/environments) for all base URLs.
```bash theme={null}
export DROPLINKED_API=https://apiv3.droplinked.com # prod
# export DROPLINKED_API=https://apiv3dev.droplinked.com # dev sandbox
```
## 2. Fetch a shop
```bash theme={null}
curl "$DROPLINKED_API/shops/v2/public/name/{shopName}"
```
Returns the shop's profile, currency, payment methods, and design template.
## 3. List the shop's products
```bash theme={null}
curl "$DROPLINKED_API/product-v2/public/shop/{shopName}?page=1&limit=24"
```
Returns the public product collection (physical, digital, and POD), with pricing and media —
everything you need to render a catalog or feed it to an agent.
## 4. Go further
Get a merchant JWT to manage shops, products, carts, and orders.
Explore every endpoint interactively in the API reference.
Expose the catalog to AI agents with the MCP server and ACP feed.
Live API health endpoint.