Skip to main content
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:
StateMeaning
PENDINGMerchant has clicked the CTA. Awaiting SUPER_ADMIN review.
APPROVEDOperator 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.
MINTEDSchema A attestation is on-chain. attestationUid + mintedAt are populated and the widget can quote the EAS reference without a second round-trip.
REJECTEDOperator 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

ParamTypeInNotes
shopSlugstringpathKebab-case brand slug — same value as BrandAttestation.brandSlug
notesstringbody, optionalFree-text merchant note. Max 2048 chars. Surfaces on the operator queue only.

Curl example

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)

{
  "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

StatusWhen
400shopSlug invalid or notes exceeds 2048 chars
404No 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

curl -s https://apiv3.droplinked.com/v2/attestations/brand/unstoppable/request-status | jq .

Response (200, history exists)

{
  "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)

{
  "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

FieldTypeNotes
statusenumNONE | PENDING | APPROVED | MINTED | REJECTED
request.requestIdstringMongo _id of the latest queue row
request.shopSlugstringNormalised lowercase slug
request.statusenumPENDING | APPROVED | MINTED | REJECTED — never NONE (that state implies request: null)
request.attestationUidstring | null0x-prefixed EAS attestation UID. Only populated when status === "MINTED"
request.mintedAtISO 8601 | nullWhen the Schema A mint succeeded on-chain. Only populated when status === "MINTED"
request.createdAtISO 8601 | nullWhen 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:
type BrandAttestationStatus = "NONE" | "PENDING" | "APPROVED" | "MINTED" | "REJECTED";

interface StatusResponse {
  status: BrandAttestationStatus;
  request: {
    requestId: string;
    shopSlug: string;
    status: Exclude<BrandAttestationStatus, "NONE">;
    attestationUid: string | null;
    mintedAt: string | null;
    createdAt: string | null;
  } | null;
}

const BASE = "https://apiv3.droplinked.com";

async function fetchStatus(shopSlug: string): Promise<StatusResponse> {
  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;
}