Skip to content

Collectibles, trades and ownership ​

8. Collectibles ​

Source: routes/v1/usercollectible_route.ts → usercollectible_controller.ts

Two of these look interchangeable and are not. 8.1 is the catalogue: every available, non-archived collectible the brand has, each carrying a user_collectibles[] array that is empty when the customer does not own it — which is what lets the vault draw locked cards beside owned ones. 8.2 is the holding: only rows the customer owns, and only those with an APPROVED status. A trade is keyed by the id of an owned row, so anything that offers a card must read 8.2; anything that draws a collection can read 8.1.

#EndpointAuthHandlerParams
8.1GET /api/v1/brand/mobile/collectiblesCustomergetAllCollectiblequery tag — the brand's full collectible catalogue, annotated with what the user owns
8.2GET /api/v1/mobile/user/collectiblesCustomergetAllMobileUserCollectiblequery tag — only what the user owns
8.3GET /api/v1/mobile/user/collectible/:idCustomergetUserCollectibleByIdpath id (number) ✅ — id of the user_collectible row
8.4GET /api/v1/mobile/brand/collectible/:idBrandgetCollectibleByIdpath id (number) ✅ — id of the collectible definition
8.5GET /api/v1/brand/mobile/user/collectibles/special-assetsCustomergetAllUserCollectiblesWithSplAssetsnone
8.6GET /api/v1/mobile/open/user/collectible/:user_idBrandget_all_usercollectibles_openpath user_id (number) ✅, query tag — public profile view of another user's collection

⚠️ 8.6 fails rather than empties. When the target customer has show_collections_for_trade off it throws "User not allowed to show collectibles for trade", which handleCatch returns as HTTP 200 with success: false. A client that reads only result cannot tell a hidden collection from an empty one.

8.7 Collectible leaderboards ​

  • GET /api/v1/mobile/user/collectibles/leaderboards — Brand
  • GET /api/v1/user/mobile/user/collectibles/leaderboards — Customer (adds the caller's own rank)

Handler: get_collectibles_leaderboards (one handler, two mounts). Query: campaignId (number, optional).

campaignId does not just filter — it changes what is counted:

Without campaignIdWith campaignId
Set countedevery collectible row the customer holdsonly the collectibles named by that campaign's steps
collectibles_countrow count, duplicates includeddistinct collectible_id count
total_collectibles_countabsentthe size of the campaign's set — the denominator in a 3 / 5

The campaign's set is read off its steps: each entry in the campaign's rewards[] whose detail.constraints[] carries restriction: "collectible_id" contributes its value. A campaign with no steps is rejected with "No rewards found for the given campaign" rather than returning an empty board.

Rows are ranked by count descending, then by name. The customer mount marks the caller with current_user: true and keeps that row in the response even when it falls outside the top of the list.


9. Trades ​

Source: routes/v1/trade_route.ts → trade_controller.ts · Auth: Customer on all.

Four types, and the difference that matters is not open versus direct but whether anything is asked in return:

typeNames a recipientAsks for a card backWhere it lands
open_tradenoyesthe board, status: open
direct_tradereceiverUserId ✅yesthat person's hub, status: open
open_giftnonothe board, status: open — first claim wins
direct_giftreceiverUserId ✅nonowhere. It completes at creation

status is its own vocabulary — open, inprogress, completed, expired, rejected, cancelled — and is never sent by a client. See the enum reference.

Two ids, easily confused. user_collectible_id and receiving_usercollectible_id are rows someone owns. The entries in desired_collectible_ids are definitions, keyed by collectible_id. A reply is matched by comparing the offered row's collectible_id against that list, so any copy of a wanted collectible satisfies it.

open_to_trade gates the caller, on both create and reply: with it off, either answers "User Not Open To Trade". It is read off the person making the call, so it stops you trading rather than hiding you from others.

9.1 POST /api/v1/brand/mobile/trade — create ​

Handler: create_trade · Body payload:

fieldtypereqnotes
typeenum✅tradeTypes: open_trade, direct_trade, open_gift, direct_gift
uidstring✅client-generated trade uid
user_collectible_idnumber✅the item you are offering
desired_collectible_idsarrayconditionalrequired for open_trade/direct_trade, forbidden for gifts. Items: { name ✅, collectible_id ✅, asset ✅ }
receiverUserIdnumberconditionalrequired for direct_trade/direct_gift, forbidden for open ones
expirationISO date—absent means the listing never lapses

A direct_gift is completed by this call: the collectible moves to receiverUserId immediately, any other open trade offering the same user_collectible_id is marked expired, and the response comes back with status: "completed". Every other type is created open.

One card, one listing: a second open trade on a user_collectible_id that already has one is refused with "Same User Collectible Already Exists for Trade with open status".

9.2 GET /api/v1/brand/mobile/trades — the open-trade board ​

Handler: get_all_open_trades · Query: search (string, "" allowed), limit (>0) and offset (≥0) — limit and offset must be sent together (.and).

Returns open_gift and open_trade rows only, at status: open, whose expiration is null or still in the future, newest-updated first. Each row carries the offered user_collectible (with its collectible's name and description) and the owner's username and user_profile.

search is one term matched three ways: the trade uid, the collectible name, and the owner's username.

⚠️ This GET mutates. Before it selects anything it sweeps every open trade past its expiration into expired, inside the same transaction. A listing expires because somebody loaded the board, not on a schedule — so on a quiet brand, expired listings persist until the next read.

There is no type filter. The validator accepts search, limit and offset and nothing else, so a board that offers "open trade / open gift / all" is filtering client-side, over the page it already holds. Combined with limit/offset that hides rows instead of fetching them.

9.3 GET /api/v1/brand/mobile/trade/:trade_id ​

Handler: get_trade_by_id · Path: trade_id (number) ✅.

9.4 GET /api/v1/brand/mobile/trade-logs ​

Handler: get_all_trade_logs · Query — both required:

  • participant ✅ — sender | receiver | both
  • type ✅ — active | completed | history

type selects a set of statuses, and the same set is applied to the parent trade as well as the log row, so a log only appears when its trade agrees:

typeStatusesThen
activeinprogress, open, rejecteddrops direct_gift rows that are rejected
historycompleted, expired, rejected, cancelleddrops every rejected row that is not a direct_gift
completedcompleted—

⚠️ The second filter is easy to miss: a rejected open_trade or direct_trade appears under active, never under history. Do not build a "past trades" screen on history alone and expect rejections in it.

Soft-deleted rows are included (paranoid: false), which is how a cancelled listing still shows up. There is no completed-trades counter endpoint — the number on the hub is the length of participant=both&type=completed.

9.5 DELETE /api/v1/brand/mobile/trade/:trade_id — take a listing down ​

Handler: delete_trade · Path: trade_id (number) ✅.

Only the owner, only at status: open, and only for open_trade, open_gift or direct_trade — a direct_gift completed at creation and cannot be recalled. Anything else is "Invalid Open Trade". The row is set to cancelled and soft-deleted rather than removed.

9.6 POST /api/v1/brand/mobile/trade/reply — claim, offer, accept, reject ​

Handler: reply_on_trade · Body payload:

fieldtypereqnotes
trade_idnumber✅
toUserIdnumber✅trade owner
typeenum✅tradeTypes
receiving_usercollectible_idnumberconditionalrequired for open_trade/direct_trade; forbidden when reject_trade: true or for gift types
accept_offerboolean—completes only when the caller is the trade's owner
reject_tradeboolean—cannot be sent together with accept_offer

One endpoint, four outcomes. Which one you get is decided by the trade's type and by whether the card you offered is on the wanted list — not by a field you set:

SituationResultcurrent_trade_log_status
type is open_giftthe collectible transfers; the trade completescompleted
reject_trade: truelogs a rejection, and notifiesrejected
Offered card's collectible_id is in desired_collectible_idsboth collectibles transfer; the trade completescompleted
Anything elsea counter offer is recorded; nothing movesinprogress

Read current_trade_log_status off the response — it is how a client tells an instant swap from an offer that is now waiting.

Further constraints, all enforced server-side:

  • A direct_gift can never be replied to; it is excluded from the lookup.
  • The trade must be open or inprogress, and the caller must be either its owner or the toUserId named.
  • Both collectibles must be APPROVED and held by the side offering them.
  • Rejecting flips the trade to rejected only for direct_trade. Reject an offer on an open_trade and the listing stays open for the next person.
  • A completed transfer sets the collectible's source to transfer.

Notifications fire per outcome: TRADE_INIT when a direct_trade is created, TRADE_COUNTER on a counter, TRADE_REJECTED on a rejection, and COLLECTIBLE_REDACTED when a direct_gift hands a card over.


10. Product ownership ​

Source: routes/v1/productownership_route.ts → productownership_controller.ts · Auth: Customer on all.

#EndpointHandlerBody payload / params
10.1POST /api/v1/mobile/request/product/ownershiprequestProductOwnershipbrl_id (number) ✅ — claim a product someone else already registered
10.2GET /api/v1/mobile/product/ownership/:idgetProductOwnershipByIdpath id (number) ✅
10.3POST /api/v1/mobile/approve-reject/product/ownershipapproveOrRejectProductOwnershipid (number) ✅ + exactly one (xor) of approve / reject (booleans)
10.4POST /api/v1/mobile/initiate/transfer/product/ownershipinitiateProductTransferbrl_id (number) ✅, email (email) ✅ — sends an OTP to the recipient
10.5POST /api/v1/mobile/transfer/product/ownershiptransferProductOwnershipid (number) ✅, otp (string) ✅ — completes 10.4

Batch platform, Shopify app and API documentation