// Soul SDK type definitions.
// https://www.soulverified.com/soul.d.ts
//
// The SDK ships as one plain script tag with no build step, so these are here for
// editors and for anyone who wants their integration typechecked:
//
// ///
//
// Every field below is what the API actually returns. If something is not in this
// file, it is not part of the contract.
declare namespace Soul {
/** soul = curated by Soul, shared. hosted = that partner's users only.
* collab = the host plus every partner named in `collab`.
* takeover = open, branded to the host. exclusive = Soul-run, capped. */
type GatheringKind = 'soul' | 'hosted' | 'collab' | 'takeover' | 'exclusive';
interface Gathering {
id: string;
title: string;
/** already formatted for display, e.g. "Tomorrow 6:30 am" */
when: string;
where: string;
startsAt: string;
going: number;
/** has the current user committed */
mine: boolean;
/** first names only, capped at six */
goers: string[];
note?: string;
/** why this surfaced for this member: the tags that scored it */
why?: string[];
kind: GatheringKind;
brandNote?: string;
/** how many of YOUR OWN users are going. Never who. */
yours?: number;
host?: string;
collab?: string[];
/** can this be checked into right now. early = not yet, open = it is happening,
* closed = the window has shut and the record is final. */
checkin: CheckinState;
/** the current user already said they were here. Stamped once, never taken back. */
arrived?: boolean;
/** how many people said they were actually there. Absent before the window opens,
* because "0 here" for something that has not started is the clock, not a fact. */
here?: number;
}
type CheckinState = 'early' | 'open' | 'closed';
interface CheckinResponse {
ok: true;
arrived: true;
/** when the arrival was stamped. Unchanged on a repeat call. */
at: string;
here: number;
sandbox?: true;
}
/** 409 from checkin(): outside the gathering's window. Both ends are given so your UI
* can say which side of it the user is on. */
interface CheckinClosed {
error: string;
code: 'checkin_not_open' | 'checkin_closed';
opensAt: string;
closesAt: string;
}
interface Person {
name: string;
interests: string[];
/** tags this person and the viewer both have */
shared?: string[];
/** overlap strength, 0-100 */
match?: number;
}
interface EventsResponse {
ok: true;
/** present only in the free sandbox */
sandbox?: true;
connected: boolean;
memberCount: number;
signal: string[];
events: Gathering[];
people: Person[];
}
interface RsvpResponse { ok: true; going: boolean; count: number }
interface ConnectResponse { ok: true; connected: string; affinities: string[] }
interface HostResponse {
ok: true;
sandbox?: true;
note?: string;
event: { id: string; title: string; where: string; when: string; startsAt: string; city: string; host: string; collab: string[]; kind: GatheringKind };
}
/** Never rejects. A dead network or a bad key resolves to this. */
interface ErrorResponse { error: string; code?: 'subscription_required'; state?: string; manage?: string }
interface InitOptions {
/** your slug. any unregistered string runs in the free sandbox. */
partner: string;
/** only needed for a registered slug in production */
key?: string;
/** YOUR user id. never an email, never a name. */
user?: string;
city?: string;
/** your product's display name, shown inside the tab */
name?: string;
/** brand colour, e.g. "#7fd0c0" */
accent?: string;
theme?: 'light' | 'dark';
/** preset visual skin, chosen independently of the slug */
skin?: string;
tag?: string;
}
interface MountOptions extends Partial {
el?: string | Element;
target?: string | Element;
/** default 640 */
height?: number;
/** default "20px" */
radius?: string;
/** default true. false = fixed height, scrolls internally. */
autosize?: boolean;
}
interface HostInput {
title: string;
/** a free, public, walk-in place */
where: string;
/** ISO date, must be in the future */
when: string;
note?: string;
city?: string;
capacity?: number;
kind?: GatheringKind;
/** other partner slugs. naming anyone makes it a collab. */
collab?: string[];
brandNote?: string;
}
type EventName = 'ready' | 'join' | 'rsvp' | 'checkin' | 'connect' | 'open' | 'close' | 'badge';
/** SERVER SIDE events, delivered to the endpoint you register. These are not the
* browser hooks above: these reach your backend. */
type WebhookEvent = 'member.joined' | 'rsvp.created' | 'rsvp.cancelled' | 'member.arrived' | 'gathering.created';
interface WebhookDelivery {
id: string;
type: WebhookEvent;
created: string;
partner: string;
data: Record;
}
}
interface SoulSDK {
version: string;
init(options: Soul.InitOptions): SoulSDK;
/** never includes the key */
config(): Omit;
Community(options: Soul.MountOptions): HTMLIFrameElement | null;
mount(target: string | Element, options?: Soul.MountOptions): HTMLIFrameElement | null;
badge(options: Soul.MountOptions & { onClick?: boolean }): HTMLElement | null;
open(options?: Soul.MountOptions & { maxWidth?: string; height?: string }): HTMLElement;
close(): void;
events(options?: { city?: string; limit?: number } & Partial): Promise;
rsvp(eventId: string, going?: boolean, options?: Partial): Promise;
/** "I'm here." Only succeeds inside the gathering's own window (30 min before, until
* 3 hours after); outside it you get a 409 with opensAt/closesAt. Idempotent: the
* arrival is stamped once and a second call returns the same time. */
checkin(eventId: string, options?: Partial): Promise;
join(options?: Partial): Promise<{ ok: true; joined: true } | Soul.ErrorResponse>;
identify(user: string, traits?: { tags?: string[]; interests?: string[]; name?: string; city?: string; source?: string }): Promise;
connect(source: string, options?: Partial): Promise<{ ok: true; source: string } | { error: string; url?: string }>;
host(event: Soul.HostInput): Promise;
on(event: Soul.EventName, fn: (payload: unknown) => void): SoulSDK;
off(event: Soul.EventName, fn?: (payload: unknown) => void): SoulSDK;
}
declare const Soul: SoulSDK;
interface Window { Soul: SoulSDK }