TesseraDocs

API & Integrations

Live webhooks when a raffle opens or a project is added, plus the read feed and partner write API.

Three surfaces, one header (X-API-Key). Partner write keys you mint yourself: one user key on your profile (or Dashboard → Keys) — the secret starts with your Tessera username — or a team key (Team → Settings). Event and read-feed keys are still issued by Tessera.

ForBase
Live eventsBots and up-pointers. Tessera POSTs when a raffle goes live or a project is added./events/v1
Read feedPoll raffles, community stats, runners, requirements, winners (CollabOS is on this)./provider
Partner writeCreate raffles and pull winners after draw, as a team member./partner/v1
POST  https://api.tesserapp.org/events/v1/webhooks
GET   https://api.tesserapp.org/events/v1/feed
GET   https://api.tesserapp.org/provider/ping
GET   https://api.tesserapp.org/provider/communities
GET   https://api.tesserapp.org/provider/communities?x={handle}
GET   https://api.tesserapp.org/provider/raffles
GET   https://api.tesserapp.org/provider/raffles/{raffleId}
GET   https://api.tesserapp.org/provider/raffles/{raffleId}/winners

Authentication

Send your key on every request in the X-API-Key header (Authorization: Bearer <key> works too). Event keys, read-feed keys, and partner keys are separate — ask for the one you need.

Same key for create and winners

The key you mint for POST /partner/v1/raffles also auths the read feed:

GET https://api.tesserapp.org/provider/raffles/{id or slug} GET https://api.tesserapp.org/provider/raffles/{id or slug}/winners

Partner winners (team-scoped, 409 until drawn):

GET https://api.tesserapp.org/partner/v1/raffles/{id or slug}/winners

curl -H "X-API-Key: YOUR_KEY" \
  "https://api.tesserapp.org/provider/raffles?status=active&limit=100"

GET /provider/ping and GET /events/v1/ping are unauthenticated — connectivity only.

Communities

Public team-page stats. Same read key as the raffle feed. A daily pass is enough. The cap is 60 requests a minute.

Look up one community by its project X:

GET /provider/communities?x=joinsyndicate_

x can be a handle, @handle, or an x.com / twitter.com profile URL. One team per handle. No match is 404. A value that is not a handle is 400.

Walk every community when you do not have the handle yet. Oldest first. cursor is the nextCursor from the previous page. limit defaults to 50 and stops at 100.

GET /provider/communities?limit=50

FieldMeaning
xHandleProject X linked on the team. Null when they have not linked one.
membersTeam members plus the owner. A count. Member handles are not on the public team page.
rafflesEvery raffle on the team.
liveRaffles still open.
stats.totalEntriesEntries on raffles whose entry list is public. Same counter as the public lists, so a live raffle can be about an hour ahead of this number.
stats.totalWinnersWinner rows on those raffles.
stats.avgEntriesPerRaffleRounded. Private entry lists are left out, same as the team page.
stats.winRateWinners divided by entries, as a rounded percent.
statsHiddenThe team hid the four stats. stats is null. Members, raffles, and live stay.
urlhttps://tesserapp.org/teams/{slug}

The body includes "source": "tessera".

Live events

HTTPS webhooks. You register a URL; Tessera POSTs JSON the moment a raffle opens or a project first appears. No websocket. If you cannot expose a URL, poll GET /events/v1/feed.

EventWhendata
raffle.liveA raffle is created — it is live at that moment.Raffle id, public URL, title, times, winner count, project X, team, requirements.
project.addedFirst raffle for that collab project's X handle. The public page /project/{handle} now exists.Handle, project URL, X URL, first raffle.

A raffle for a new project fires both events. Treat them as independent POSTs — delivery order is not guaranteed.

Register a webhook

curl -X POST https://api.tesserapp.org/events/v1/webhooks \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-bot.example/tessera","events":["raffle.live","project.added"]}'

Response includes id, url, events, and secret once. Store the secret. Later GET /events/v1/webhooks does not return it. Max 5 webhooks per key. HTTPS only.

DELETE /events/v1/webhooks/{id} removes it. POST /events/v1/webhooks/{id}/test sends a webhook.ping so you can check the path before a real raffle.

Receive the POST

Tessera POSTs to your URL with a JSON body and these headers:

HeaderMeaning
X-Tessera-Eventraffle.live or project.added (or webhook.ping on test)
X-Tessera-IdEvent id (evt_…) — idempotent key
X-Tessera-TimestampUnix seconds
X-Tessera-Signaturesha256= + HMAC-SHA256 of the raw body with your webhook secret

Reply 2xx within 8 seconds. Timeouts and non-2xx retry twice. After 20 consecutive failures the webhook is disabled (active: false). We do not follow redirects.

Verify the signature

import crypto from 'node:crypto'
 
function ok(rawBody, secret, header) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex')
  return crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected))
}

Use the raw bytes, not a re-serialized object.

Body

{
  "id": "evt_…",
  "type": "raffle.live",
  "createdAt": "2026-09-16T18:00:00Z",
  "data": {}
}
PropTypeDefault
id
string
-
type
'raffle.live' | 'project.added' | 'webhook.ping'
-
createdAt
ISO timestamp
-
data
object
-

raffle.live data

PropTypeDefault
raffleId
string
-
raffleUrl
string
-
title
string
-
status
'active'
-
createdAt / startAt
ISO timestamp
-
endAt
ISO timestamp
-
winnerCount
number
-
requiredWalletType
string | null
-
projectTwitter
string
-
projectUrl
string
-
team
object | null
-
runner
object
-
requirements
object[]
-

project.added data

PropTypeDefault
handle
string
-
projectUrl
string
-
xUrl
string
-
firstRaffleId
string
-
firstRaffleUrl
string
-
title
string
-

Poll instead

GET /events/v1/feed?since={ISO}&type=raffle.live&limit=50 — same envelopes, oldest first, last ~200 events. Use since as the previous createdAt. Still needs X-API-Key.

This is not a Discord webhook

Team Settings → webhook URL posts raffle embeds into a Discord channel. /events/v1 is for your own bot: you give Tessera an HTTPS endpoint, we POST the JSON above. Discord webhook URLs are the wrong target unless you wrap them yourself.

Read feed

Poll when you need the full raffle list or winners. For “it just went live”, use Live events above.

List raffles

GET /provider/raffles — every raffle on Tessera, newest first, paginated.

ParameterDefaultDescription
statusallall, active, or ended
limit100Page size, max 200
offset0Pagination offset
updated_since—ISO timestamp — only raffles created or announced after this

Raffle fields

PropTypeDefault
raffleId
string
-
raffleUrl
string
-
title
string
-
status
'active' | 'ended'
-
createdAt / startAt
ISO timestamp
-
endAt
ISO timestamp
-
endedAt
ISO timestamp | null
-
updatedAt
ISO timestamp
-
raffleType
'standard'
-
entryCount
number
-
participantCount
number
-
hostedFor
object
-
project
object | null
-
allocation
object
-
runner
object
-
requirements
object[]
-

Hosted by, hosted for, project

The raffle page shows three identities. GET /provider/raffles and GET /provider/raffles/{id|slug} use the same three.

On the pageFieldWhat it is
Hosted byrunnerThe person who created the raffle
Hosted forhostedForThe Tessera community
ProjectprojectThe collab project X

On Farmerz DAO x Ascendant, Hosted by is SUNNIKA (runner), Hosted for is Farmerz Club (hostedFor), and Project is Ascendants @ascendantsgames (project).

project is null when the raffle has no project X. createdBy, managedBy, and drawnBy still mirror runner.

Runner identity

Every raffle carries the actual person who runs it, keyed the way integrations need it:

FieldPurpose
runner.userIdStable Tessera user ID
runner.displayNameHuman-readable name
runner.discordIdBest key for automatic matching to your workspace members
runner.discordUsernameDisplay and fallback identity
runner.xUserId / runner.xUsernameX identity where connected

createdBy, managedBy and drawnBy mirror runner — Tessera has a single raffle role (the creator). drawnBy is null until winners are drawn.

Requirements

The requirements array is pass-through from Tessera's own model — map what you need:

TypeMeaning
discord_linkedEntrant must link a Discord account
discord_serverRole / server requirement — carries guildId, guildName, roleIds[], roleNames[]
twitter_followX follow — value is the handle (@user or user)
twitter_tweetAlias of twitter_both (like & repost). value is the tweet URL or numeric id. Optional tweetId — filled from value if omitted
twitter_bothLike & repost the tweet. Same fields as twitter_tweet
twitter_likeLike the tweet. Same value / tweetId fields
twitter_retweetRepost the tweet. Same value / tweetId fields
twitter_replyReply to the tweet. Same value / tweetId fields
…Wallet checks and others pass through the same shape

Winners

GET /provider/raffles/{raffleId} (or the URL slug) returns the raffle plus:

PropTypeDefault
winners
object[]
-
requiredWalletType
string | null
-
winnerCount
number
-
winnersPublic
boolean
-

Winners are gated by the team's own choice

Winner lists are included only when the hosting team publishes winners. A companion flag (enabled by Tessera) can widen that to all drawn raffles. Winner private codes and private winner notes are never part of the payload.

Errors

StatusMeaning
401Missing or invalid X-API-Key
403Key cannot use that team (wrong team key, or user key not on that roster)
404Team / raffle does not exist, or the path is unknown
400Bad parameter (e.g. malformed updated_since)

Rate guidance: paginate with limit ≤ 200 and walk offset; prefer updated_since for incremental polling.

Partner write API

For bots that create raffles and pull winners after draw (not the CollabOS read feed). Auth is X-API-Key.

Two kinds of keys — mint them in the app, secret shown once:

KindWhereScope
UserDashboard → Keys · profile · POST /user/api-keysEvery team you can manage. One key. Secret starts with your Tessera username.
TeamTeam → Edit → Settings · POST /teams/{teamId}/api-keysThat community only

A user key on Sanctuary does not fail with a silent 404 on another community you belong to — it works there too. A team key for Sanctuary returns 403 if you send a different teamId. A team that does not exist is 404. A missing route is 404 Not Found.

GET   https://api.tesserapp.org/partner/v1/ping
GET   https://api.tesserapp.org/partner/v1/teams
GET   https://api.tesserapp.org/partner/v1/templates?teamId=
POST  https://api.tesserapp.org/partner/v1/raffles
GET   https://api.tesserapp.org/partner/v1/raffles/{id|slug}
GET   https://api.tesserapp.org/partner/v1/raffles/{id|slug}/winners
GET   https://api.tesserapp.org/partner/v1/raffles/{id|slug}/winners.csv

Mint / list / revoke (session cookie, same as the site):

GET/POST/DELETE  https://api.tesserapp.org/user/api-keys
GET/POST/DELETE  https://api.tesserapp.org/teams/{teamId}/api-keys

Create

POST /partner/v1/raffles — same fields as the site create form.

FieldDefaultNotes
titlerequired
endsAtrequiredISO timestamp
teamIdrequired for user keysMust be a team this key can use (GET /teams). Team keys default to their locked team.
winnerCount1
requirements[]Same objects as the UI (type, label, value, tweetId, guildId, …). twitter_tweet is stored as twitter_both
requiredWalletType"EVM"EVM / SOL / BTC / ZEC / NEAR / SUI / XMR / null
autoDrawWinnersfalseEnded is not the same as drawn
postDiscordfalseIf true, needs a Discord channel (discord.channelId or templateId with a post channel). Create is live — no extra status/activate call.
templateId—Team template UUID. Copies that template’s Discord channel + requirements when you omit them. Each Follow row includes targetUserId when the account exists.
discord—{ guildId, channelId, roleId, winnerChannelId }
raffleImageUrl / raffleBannerUrl—Raffle logo + banner. Aliases: avatarUrl, bannerUrl.

Follow handles do not have to be looked up on X before create. GET /partner/v1/templates returns targetUserId on each twitter_follow row when Tessera can see the profile. GET /partner/v1/x/lookup?handle= returns { found, handle, userId } with status 200. found: false means that profile page is gone. An X API body of User not found is not a reason to skip the community — post the raffle with the handle, or with targetUserId when it is set. Create still saves the follow if that id is missing.

Status

GET /partner/v1/raffles/{id} is safe to poll anytime. It returns status (active / ended), endsAt, drawn (bool), announcedAt, entryCount. No winner list. No entries.

Winners

GET …/winners and …/winners.csv return the drawn Winner table only.

  • While the raffle is live: 409 { "detail": { "ready": false, "reason": "not_ended" } }
  • Ended, but winners are still pending (pre-draw) or there are none: 409 not_drawn
  • A short draw still returns. If the raffle ended and every saved winner is confirmed, you get that list even when fewer people won than winnerCount
  • Never returns in-progress entrants as winners
  • The public read feed (GET /provider/raffles/{id}/winners) stays empty until winners are announced or the host marks them public. Partner winners do not wait for that announce
  • wallet is the payout address for the raffle’s current requiredWalletType. If the chain changes after people enter (EVM → SOL), wallet is the SOL address, not the 0x saved at entry. The address they entered is kept only when it still matches that chain.
  • Each winner also includes the profile wallets: walletAddress (EVM), solWalletAddress, btcWalletAddress, zecWalletAddress, nearWalletAddress, suiWalletAddress, xmrWalletAddress. The read feed (GET /provider/raffles/{id}/winners) uses the same fields. Sui addresses are 0x plus 64 hex. Monero addresses are mainnet, start with 4 or 8, and keep their original case.
  • The JSON body includes requiredWalletType.

CSV columns: rank, username, discordId, xHandle, wallet, tierName, status, then the profile wallet columns (walletAddress, solWalletAddress, btcWalletAddress, zecWalletAddress, nearWalletAddress, suiWalletAddress, xmrWalletAddress). wallet is the payout column.

Next

On this page