Gather Developer API
Create collectible cards and drops with code.
You're a developer or an AI agent. You have images and ideas. The Gather API lets you turn them into tradeable digital collectible cards that fans can open in packs — no app needed, just GraphQL.
Two API calls to a live drop
One mutation creates all cards, variations, packs, and publishes the drop.
How It Works
Gather uses a simple content model. Everything chains together:
Connect Your API Key
Every request needs your API key in the Authorization header. Your key is tied to your brand — you can only create content for your own brand.
# Set these once — reuse everywhere
ENDPOINT="https://<your-appsync-id>.appsync-api.us-east-1.amazonaws.com/graphql"
API_KEY="gth_your_api_key_here"
curl -X POST "$ENDPOINT" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "...", "variables": { ... }}'
gth_ followed by 48 characters
Authorization: Bearer gth_...
Don't have a key yet? Contact the Gather team to get an API key for your brand.
Upload Your Images
Get your card artwork and drop cover into Gather's CDN.
Before you can create a drop, upload the images you'll use. This is a two-step process per image: ask for an upload URL, then PUT your file to it.
Ask for an upload URL
mutation {
requestUploadUrl(input: {
brandID: "BRAND_abc123"
kind: "card-image"
fileName: "rookie-debut.png"
}) {
uploadUrl
publicUrl
contentType
}
}
You'll get back:
uploadUrl— a temporary presigned S3 URL. PUT your file here.publicUrl— the permanent CDN link. Save this — you'll use it in your drop.contentType— the MIME type to use in the PUT request (auto-detected from file extension).
Upload the file
# Use uploadUrl and contentType from the response above
curl -X PUT "${uploadUrl}" \
-H "Content-Type: ${contentType}" \
--data-binary @rookie-debut.png
publicUrl — you'll use them in the next step.
Create the Drop
One mutation. Cards, variations, packs, and publish — all at once.
quickCreateDrop is the fastest way to go from images to a live drop. You define your card designs, their rarity/finish variations, and pack configuration in a single call. The API creates everything for you.
mutation {
quickCreateDrop(input: {
brandID: "BRAND_abc123"
brandName: "My Brand"
title: "Season 1 — Rookie Class"
description: "The debut collection featuring 2 rookies"
coverImageUrl: "https://cdn.gather.fan/drops/s1-cover.png"
maxPacksPerUser: 5
autoPublish: true
cards: [
{
name: "Rookie Debut"
description: "The first card in the 2025 series"
imageUrl: "https://cdn.gather.fan/cards/rookie-debut.png"
tags: ["basketball", "rookie", "2025"]
template: IMAGE_ONLY
variations: [
{ rarity: COMMON, finish: NON_HOLO, supply: 200 }
{ rarity: UNCOMMON, finish: REVERSE_HOLO, supply: 100 }
{ rarity: RARE, finish: CHROME, supply: 50 }
{ rarity: EPIC, finish: GOLD_FOIL, supply: 20 }
{ rarity: LEGENDARY, finish: HOLOGRAPHIC, supply: 5 }
]
}
{
name: "Rising Star"
description: "Second pick of the draft"
imageUrl: "https://cdn.gather.fan/cards/rising-star.png"
tags: ["basketball", "star", "2025"]
template: IMAGE_ONLY
variations: [
{ rarity: COMMON, finish: NON_HOLO, supply: 200 }
{ rarity: UNCOMMON, finish: REVERSE_HOLO, supply: 100 }
{ rarity: RARE, finish: CHROME, supply: 50 }
{ rarity: EPIC, finish: GOLD_FOIL, supply: 20 }
{ rarity: LEGENDARY, finish: HOLOGRAPHIC, supply: 5 }
]
}
]
packTemplates: [
{
name: "Standard Pack"
tier: "standard"
supply: 400
cardsPerPack: 3
guaranteedMinRarity: "COMMON"
price: 0
}
{
name: "Premium Pack"
tier: "premium"
supply: 100
cardsPerPack: 5
guaranteedMinRarity: "RARE"
price: 4.99
}
]
rarityDistribution: {
common: 70
rare: 25
legendary: 5
}
}) {
drop {
dropID
dropName
totalPacks
VISIBILITY_STATUS
CREATION_STATUS
collectibles { cardID name rarity finish supply }
packTemplates { name tier supply cardsPerPack }
}
cardsCreated { cardID name rarity finish baseCardID }
}
}
{
"data": {
"quickCreateDrop": {
"drop": {
"dropID": "a1b2c3d4-...",
"dropName": "Season 1 — Rookie Class",
"totalPacks": 500,
"VISIBILITY_STATUS": "VISIBLE",
"CREATION_STATUS": "READY",
"collectibles": [
{ "cardID": "...", "name": "Rookie Debut", "rarity": "COMMON", "finish": "NON_HOLO", "supply": 200 },
{ "cardID": "...", "name": "Rookie Debut", "rarity": "LEGENDARY", "finish": "HOLOGRAPHIC", "supply": 5 }
],
"packTemplates": [
{ "name": "Standard Pack", "tier": "standard", "supply": 400, "cardsPerPack": 3 },
{ "name": "Premium Pack", "tier": "premium", "supply": 100, "cardsPerPack": 5 }
]
},
"cardsCreated": [
{ "cardID": "...", "name": "Rookie Debut", "rarity": null, "finish": null, "baseCardID": null },
{ "cardID": "...", "name": "Rookie Debut", "rarity": "COMMON", "finish": "NON_HOLO", "baseCardID": "..." }
]
}
}
}
With autoPublish: true, the drop is immediately visible to fans. Cards, variations, pack tiers — everything was created in that single call.
What happens behind the scenes
cards
Each card entry creates a base template plus one variation per rarity/finish combo. All cards are stored in DynamoDB.
variations
Each { rarity, finish, supply } triple becomes a collectible card in the drop. Supply controls how many of that variation exist.
packTemplates
Defines different pack tiers. Each tier has its own supply, cards per pack, price, and guaranteed rarity.
autoPublish
Set to true to go live immediately. Set to false (or omit) to create the drop hidden, then publish later with publishDrop.
common: 70 = 70% chance of pulling a Common, rare: 25 = 25% Rare, legendary: 5 = 5% Legendary.
These are the odds for each card slot when a fan opens a pack.
What you just did
| Step | API Call | Count |
|---|---|---|
| Upload card images | requestUploadUrl + HTTP PUT | 1 per image |
| Upload drop cover | requestUploadUrl + HTTP PUT | 1 |
| Create everything | quickCreateDrop | 1 |
For a drop with 4 card designs and 5 variations each: 5 API calls total (4 image uploads + 1 cover upload + 1 quickCreateDrop). That's it.
Complete Script: Images to Live Drop
This is a full, runnable bash script that takes a folder of images and creates a published drop. Copy it, set your variables, and run it. This is the exact workflow an AI agent should follow.
quickCreateDrop, done. The entire process is 1 GraphQL call per image + 1 final call.
#!/usr/bin/env bash
set -euo pipefail
# ── Configuration ──────────────────────────────────────────
# Replace these with your actual values
ENDPOINT="https://<your-appsync-id>.appsync-api.us-east-1.amazonaws.com/graphql"
API_KEY="gth_your_api_key_here"
BRAND_ID="BRAND_your_brand_id"
BRAND_NAME="Your Brand"
# ── Helper: send a GraphQL request ────────────────────────
gql() {
local query="$1"
local variables="${2:-{}}"
local payload
payload=$(jq -n --arg q "$query" --argjson v "$variables" \
'{query: $q, variables: $v}')
curl -s -X POST "$ENDPOINT" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "$payload"
}
# ── Helper: upload one image ──────────────────────────────
upload_image() {
local file_path="$1"
local kind="$2" # "card-image" or "cover"
local file_name
file_name=$(basename "$file_path")
# Step A: Get a presigned upload URL
local result
result=$(gql 'mutation($input: RequestUploadUrlInput!) {
requestUploadUrl(input: $input) {
uploadUrl
publicUrl
contentType
}
}' "$(jq -cn \
--arg bid "$BRAND_ID" \
--arg kind "$kind" \
--arg fn "$file_name" \
'{input: {brandID: $bid, kind: $kind, fileName: $fn}}')")
local upload_url public_url content_type
upload_url=$(echo "$result" | jq -r '.data.requestUploadUrl.uploadUrl')
public_url=$(echo "$result" | jq -r '.data.requestUploadUrl.publicUrl')
content_type=$(echo "$result" | jq -r '.data.requestUploadUrl.contentType')
# Step B: PUT the file to S3
curl -s -X PUT "$upload_url" \
-H "Content-Type: $content_type" \
--data-binary "@$file_path" > /dev/null
echo "$public_url"
}
# ── Step 1: Upload card images ────────────────────────────
echo "Uploading card images..."
CARD1_URL=$(upload_image "images/warrior.png" "card-image")
CARD2_URL=$(upload_image "images/mage.png" "card-image")
CARD3_URL=$(upload_image "images/rogue.png" "card-image")
echo "Uploading cover..."
COVER_URL=$(upload_image "images/cover.png" "cover")
echo "All images uploaded."
# ── Step 2: Create the drop ───────────────────────────────
echo "Creating drop..."
VARIABLES=$(jq -cn \
--arg brandID "$BRAND_ID" \
--arg brandName "$BRAND_NAME" \
--arg card1 "$CARD1_URL" \
--arg card2 "$CARD2_URL" \
--arg card3 "$CARD3_URL" \
--arg cover "$COVER_URL" \
'{
input: {
brandID: $brandID,
brandName: $brandName,
title: "Fantasy Heroes Collection",
description: "Three legendary heroes. Collect them all.",
coverImageUrl: $cover,
maxPacksPerUser: 5,
autoPublish: true,
cards: [
{
name: "Shadow Warrior",
description: "A blade in the dark",
imageUrl: $card1,
tags: ["fantasy", "warrior"],
template: "IMAGE_ONLY",
variations: [
{ rarity: "COMMON", finish: "NON_HOLO", supply: 100 },
{ rarity: "RARE", finish: "HOLOGRAPHIC", supply: 20 },
{ rarity: "LEGENDARY", finish: "CHROME", supply: 3 }
]
},
{
name: "Arcane Mage",
description: "Master of the elements",
imageUrl: $card2,
tags: ["fantasy", "mage"],
template: "IMAGE_ONLY",
variations: [
{ rarity: "COMMON", finish: "NON_HOLO", supply: 100 },
{ rarity: "RARE", finish: "GALAXY", supply: 15 },
{ rarity: "LEGENDARY", finish: "GOLD_FOIL", supply: 2 }
]
},
{
name: "Night Rogue",
description: "Strikes without warning",
imageUrl: $card3,
tags: ["fantasy", "rogue"],
template: "IMAGE_ONLY",
variations: [
{ rarity: "COMMON", finish: "NON_HOLO", supply: 80 },
{ rarity: "RARE", finish: "HOLOGRAPHIC", supply: 25 },
{ rarity: "LEGENDARY", finish: "PRISMATIC", supply: 5 }
]
}
],
packTemplates: [
{
name: "Standard Pack",
tier: "standard",
supply: 100,
cardsPerPack: 5,
guaranteedMinRarity: "COMMON",
price: 0
}
],
rarityDistribution: {
common: 60,
rare: 25,
legendary: 15
}
}
}')
RESULT=$(gql 'mutation($input: QuickCreateDropInput!) {
quickCreateDrop(input: $input) {
drop {
dropID
dropName
totalPacks
totalCards
VISIBILITY_STATUS
CREATION_STATUS
collectibles { name rarity finish supply }
packTemplates { name tier supply cardsPerPack }
}
cardsCreated { cardID name rarity finish }
}
}' "$VARIABLES")
# ── Step 3: Verify ────────────────────────────────────────
DROP_ID=$(echo "$RESULT" | jq -r '.data.quickCreateDrop.drop.dropID')
STATUS=$(echo "$RESULT" | jq -r '.data.quickCreateDrop.drop.VISIBILITY_STATUS')
echo ""
echo "Drop created!"
echo " ID: $DROP_ID"
echo " Status: $STATUS"
echo ""
echo "Collectibles:"
echo "$RESULT" | jq -r '.data.quickCreateDrop.drop.collectibles[]
| " - \(.name) [\(.rarity)] \(.finish) (supply: \(.supply))"'
echo ""
echo "Done — drop is live in the Gather app!"
What this script does
upload_image
Calls requestUploadUrl to get a presigned S3 URL, then PUTs the file to it. Returns the permanent CDN URL.
quickCreateDrop
One mutation that creates 3 base cards + 9 variations + the drop + pack templates, and publishes it. All in one call.
autoPublish: true
Makes the drop visible immediately. Set to false to create it hidden, then call publishDrop when ready.
quickCreateDrop = 5 GraphQL calls. That's it for a complete, published drop with 9 collectible variations.
Key rules for AI agents
| Rule | Details |
|---|---|
| Upload images first | You must call requestUploadUrl + PUT for each image before referencing it in quickCreateDrop. The imageUrl and coverImageUrl fields expect the publicUrl returned by the upload step. |
Use the correct brandID |
Your API key is scoped to one brand. Pass that exact brandID (e.g. BRAND_abc123) in every mutation. A mismatch returns UnauthorizedError. |
rarityDistribution only has 3 fields |
The input accepts common, rare, and legendary only. These are weight percentages (e.g. 60 / 25 / 15). Cards with UNCOMMON or EPIC rarity still work — the distribution maps them to the nearest tier. |
| Max 50 variations per call | quickCreateDrop supports up to 50 total card variations across all cards. For larger drops, use multiple createCardWithVariations calls + createDrop. |
Verify with getDrop |
After creating, query getDrop(dropID) to confirm the drop is VISIBLE and all collectibles + pack templates are correct. |
Discovering your brand ID
If you don't know your brandID, query any of your existing data:
# Works with any API key — returns your brand from existing drops
query { listVisibleDrops(limit: 1) { items { brandID brandName } } }
# Or from your cards
query { listCards(limit: 1) { items { brandID brandName } } }
Step-by-Step (Advanced)
If you need more control over each step, you can use the individual mutations instead of quickCreateDrop.
Option A: createCardWithVariations + createDrop
Create cards and variations in one call per card design, then assemble the drop manually.
mutation {
createCardWithVariations(input: {
brandID: "BRAND_abc123"
brandName: "My Brand"
name: "Rookie Debut"
description: "The first card in the 2025 series"
imageUrl: "https://cdn.gather.fan/cards/rookie-debut.png"
tags: ["basketball", "rookie", "2025"]
template: IMAGE_ONLY
variations: [
{ rarity: COMMON, finish: NON_HOLO, supply: 200 }
{ rarity: RARE, finish: CHROME, supply: 50 }
{ rarity: LEGENDARY, finish: HOLOGRAPHIC, supply: 5 }
]
}) {
baseCard { cardID name }
variations { cardID rarity finish }
}
}
Then use the returned card IDs to assemble a drop with createDrop + publishDrop.
Option B: Fully manual (createCard one at a time)
Create each card individually with createCard, set baseCardID for variations, then build the drop. See the Reading Data section for how to verify each step.
mutation {
createCard(input: {
brandID: "BRAND_abc123"
brandName: "My Brand"
name: "Rookie Debut"
imageUrl: "https://cdn.gather.fan/cards/rookie-debut.png"
tags: ["basketball", "rookie"]
template: IMAGE_ONLY
}) {
cardID
name
}
}
mutation {
createCard(input: {
brandID: "BRAND_abc123"
brandName: "My Brand"
name: "Rookie Debut"
imageUrl: "https://cdn.gather.fan/cards/rookie-debut.png"
tags: ["basketball", "rookie"]
template: IMAGE_ONLY
baseCardID: "CARD_f7a2b9c1"
rarity: LEGENDARY
finish: HOLOGRAPHIC
}) {
cardID
rarity
finish
}
}
quickCreateDrop = 5 API calls for a typical drop.
createCardWithVariations + createDrop = ~9 calls.
Fully manual = ~28 calls. All three produce the same result.
Retrieve Your Data
Look up your cards, variations, and drops at any time.
brandID to filter.
List all your cards (base templates)
query {
getBrandCards(brandID: "BRAND_abc123", cardType: TEMPLATE, limit: 50) {
items {
cardID
name
imageUrl
template
status
createdAt
}
nextToken
}
}
List all variations for a brand
query {
getBrandCards(brandID: "BRAND_abc123", cardType: VARIATION, limit: 50) {
items {
cardID
baseCardID
name
rarity
finish
template
status
}
nextToken
}
}
List all your drops
query {
getBrandDrops(brandID: "BRAND_abc123") {
dropID
dropName
totalPacks
mintedPacks
VISIBILITY_STATUS
PAUSED
createdAt
packTemplates {
name
tier
supply
mintedCount
}
}
}
Get full details for a specific drop
query {
getDrop(dropID: "a1b2c3d4-...") {
dropID
dropName
totalPacks
mintedPacks
VISIBILITY_STATUS
CREATION_STATUS
collectibles {
cardID
baseCardID
name
rarity
finish
supply
mintedCount
}
packTemplates {
packID
name
tier
supply
mintedCount
cardsPerPack
price
}
}
}
Look up a single card
query {
getCard(cardID: "your-card-id-here") {
cardID
baseCardID
name
rarity
finish
template
imageUrl
tags
status
createdAt
}
}
Query Gather
Every query below works with your API key. Use them to explore your content, check drop stats, or recover lost IDs.
Cards
| Query | What it returns | Key arguments |
|---|---|---|
getCard(cardID) |
A single card — base template or variation | cardID: ID! |
getBrandCards(brandID) |
All cards for your brand, paginated | cardType: TEMPLATE | VARIATION, status, limit, nextToken |
listCards |
All cards on the platform, paginated | cardType, status, limit, nextToken |
query {
getBrandCards(brandID: "BRAND_abc123", cardType: TEMPLATE, limit: 50) {
items {
cardID
name
imageUrl
template
status
createdAt
}
nextToken
}
}
query {
getBrandCards(brandID: "BRAND_abc123", cardType: VARIATION, limit: 50) {
items {
cardID
baseCardID
name
rarity
finish
template
imageUrl
}
nextToken
}
}
Drops
| Query | What it returns | Key arguments |
|---|---|---|
getDrop(dropID) |
Full drop details including collectibles and pack templates | dropID: ID! |
getBrandDrops(brandID) |
All drops for your brand | brandID: ID! |
listDrops |
All drops sorted by start time, paginated | limit, nextToken |
listVisibleDrops |
Only published, non-paused drops | brandID (optional filter), limit, nextToken |
query {
getBrandDrops(brandID: "BRAND_abc123") {
dropID
dropName
totalPacks
mintedPacks
mintedCards
VISIBILITY_STATUS
CREATION_STATUS
PAUSED
startsAt
createdAt
}
}
query {
getDrop(dropID: "your-drop-id") {
dropID
dropName
description
coverImageUrl
totalPacks
mintedPacks
totalCards
mintedCards
maxPacksPerUser
VISIBILITY_STATUS
CREATION_STATUS
PAUSED
startsAt
endsAt
collectibles {
cardID
baseCardID
name
rarity
finish
supply
mintedCount
}
packTemplates {
packID
name
tier
supply
mintedCount
cardsPerPack
guaranteedMinRarity
price
}
rarityDistribution { common uncommon rare epic legendary }
rarityMinted { common uncommon rare epic legendary }
}
}
Collectibles & Packs
| Query | What it returns | Key arguments |
|---|---|---|
getDropCollectibles(dropID) |
All minted collectibles from a drop, paginated | dropID: ID!, limit, nextToken |
getUserCollectibles(userID) |
All collectibles owned by a specific user | userID: ID! |
getDropPacks(dropID) |
All packs minted from a drop, paginated | dropID: ID!, limit, nextToken |
getUserPacks(userID) |
All packs owned by a user | status: "SEALED" | "OPENED", limit, nextToken |
getPack(packID) |
A single pack by ID | packID: ID! |
getUserPackCount(userID, dropID) |
How many packs a user claimed from a drop | userID: ID!, dropID: ID! |
query {
getDropCollectibles(dropID: "your-drop-id", limit: 20) {
items {
collectibleID
userID
cardID
name
rarity
finish
onChainStatus
createdAt
}
nextToken
}
}
Brand Stats
| Query | What it returns | Key arguments |
|---|---|---|
getNumberCollectors(brandID) |
Total unique collectors for your brand | brandID: ID! |
getCollectibleStats |
Platform-wide stats: total, minted, pending, not minted | None |
getRecentMintActivity |
Live feed of recent pack mints across all drops | limit, nextToken |
query {
getNumberCollectors(brandID: "BRAND_abc123") {
brandID
collectorCount
}
}
Binders
| Query | What it returns | Key arguments |
|---|---|---|
getBinder(binderID) |
A single binder with slot requirements | binderID: ID! |
getBrandBinders(brandID) |
All binders for your brand, paginated | brandID: ID!, limit, nextToken |
listBinders |
All binders on the platform, paginated | limit, nextToken |
getUserBinderProgress(userID, binderID) |
A user's progress on a specific binder | userID: ID!, binderID: ID! |
limit and nextToken. Pass the nextToken from one response into the next request to get the next page.
Managing Drops After Launch
You have full control over your drop after publishing.
Pause minting
Temporarily stop pack sales
pauseDrop(dropID)
Resume minting
Restart pack sales
unpauseDrop(dropID)
Hide the drop
Remove from public view
unpublishDrop(dropID)
Update details
Change title, limits, timing
updateDrop(dropID, input)
Delete the drop
Remove permanently
deleteDrop(dropID)
Update a card
Change name, desc, status
updateCard(input)
Rarities & Finishes
Rarities
Set on each card variation. Determines pull rate in packs.
Finishes
Visual effects applied to the card. Pair with rarity for unique combinations.
Card Templates
The template field controls the visual frame around your card artwork. Each template defines the card's border style, header, name plate, footer, and accent colors. Below are blank previews of all 12 templates.
template field, cards use IMAGE_ONLY by default — a full-bleed image with no frame, header, or footer, just a floating brand badge and name overlay. Use a named template like SPORTS or GAMING for a framed card layout.
Errors
When something goes wrong, the API returns an error in the standard GraphQL format:
{
"data": null,
"errors": [{
"errorType": "UnauthorizedError",
"message": "Not authorized for this brand"
}]
}
| Error | Why it happened | Fix |
|---|---|---|
UnauthorizedError |
Wrong brandID or invalid key | Check your API key and use the correct brandID |
RateLimitExceeded |
Too many requests per minute | Wait a few seconds, then retry |
UploadError |
Bad upload kind or file name | Use one of: card-image, cover, collectible, binder-cover |
ConditionalCheckFailed |
Resource conflict (e.g. duplicate) | Check if the resource already exists |
Rate Limits
Creating a typical drop with quickCreateDrop takes about 5 calls — well within the standard tier limit. Even the manual approach (~28 calls) fits comfortably.