SDKs
Official SDKs for TypeScript, Python, Ruby, Go, PHP, Swift and Kotlin: typed, retrying, idempotent, generated from the OpenAPI document.
Every server SDK wraps the same /v1 API and is generated from its OpenAPI document, so each one has a typed method for every operation, typed models, and an error class for every error code. All of them are dependency-free.
Language | Install | Package | Client |
|---|---|---|---|
TypeScript / JavaScript |
|
| |
Python 3.11+ |
|
| |
Ruby 3.1+ |
|
| |
Go 1.23+ |
|
| |
PHP 8.1+ |
|
| |
Swift (macOS 12 / iOS 15+) | SwiftPM: | Swift Package Index |
|
Kotlin / JVM 17+ |
|
|
Pass the host without /v1 as the base URL (it defaults to the hosted API). Never ship an rk_ key inside a browser or mobile app: use the server SDKs on your backend; the Swift and Kotlin ReferallyTracker use only the public tracking token.
What every SDK does the same way
Each SDK passes the same conformance plan (packages/sdk-conformance), run against a mock server in CI:
One method per operation, named after its
operationIdwithoutV1:getPartnersId,postReferralsAssociate,get_referralsin Python and Ruby,GetPartnersIDin Go. Path params come first, then the body, then query params.Typed models for every schema:
TypedDicts (Python), RBS signatures (Ruby), PHPStan type aliases (PHP), structs (Go),Codablestructs (Swift), data classes (Kotlin).Typed errors: each documented error code has its own class (
ValidationError,NotFoundError,RateLimitedError,ReplayError, …) carryingcode,status,message,requestId,docsUrl,detailsandretryAfter. Unknown codes fall back to the base API error class.Retries: 429 and 5xx responses and network errors are retried up to 3 times with exponential backoff (500 ms, doubling, with jitter), honoring
Retry-After. 4xx errors are never retried.Idempotency: every POST carries an
Idempotency-Key, generated unless you pass one, and the same key is reused on every retry, so a retried create never runs twice.Pagination: list methods return one page (
data,has_more,next_cursor);…Each/…_iter/…_eachiterate every item, followingstarting_after.Versioning: requests send
referally-version: 2026-11-01, the version the SDK was generated against; each client lets you pin another.Webhooks: a Standard Webhooks verifier that rejects deliveries older than 5 minutes and accepts either secret during a rotation.
Server SDKs
- TypeScript
- Python
- Ruby
- Go
- PHP
- Swift
- Kotlin
import { ReferallyClient } from '@referally/sdk-js';
const referally = new ReferallyClient({ apiKey: process.env.REFERALLY_API_KEY! });
const { referral } = await referally.associate({
programId: 'prg_...',
customerRef: stripeCustomer.id,
anonId: form.referally_anon_id,
referralToken: form.referally_click_id || undefined,
idempotencyKey: `signup:${user.id}`,
});
for await (const r of referally.list('/v1/referrals', { status: 'active' })) console.log(r.id);import os
from referally import Referally, NotFoundError
client = Referally(api_key=os.environ["REFERALLY_API_KEY"])
client.associate(program_id="prg_...", customer_ref=stripe_customer.id,
anon_id=form["referally_anon_id"], idempotency_key=f"signup:{user.id}")
partner = client.post_partners({"email": "dana@example.com"})
for referral in client.get_referrals_iter(status="active"):
print(referral["id"])require "referally"
client = Referally::Client.new(api_key: ENV.fetch("REFERALLY_API_KEY"))
client.associate(program_id: "prg_...", customer_ref: stripe_customer.id,
anon_id: params[:referally_anon_id], idempotency_key: "signup:#{user.id}")
client.get_referrals_each(status: "active") { |r| puts r["id"] }client := referally.New(os.Getenv("REFERALLY_API_KEY"), "")
ref, err := client.Associate(referally.AssociateInput{
ProgramID: "prg_...", CustomerRef: stripeCustomer.ID,
AnonID: r.FormValue("referally_anon_id"), IdempotencyKey: "signup:" + user.ID,
})
for referral, err := range client.GetReferralsEach(ctx, &referally.GetReferralsParams{Status: referally.Ptr("active")}) {
// …
}$client = new Referally\Client(getenv('REFERALLY_API_KEY'));
$result = $client->associate([
'programId' => 'prg_...',
'customerRef' => $stripeCustomer->id,
'anonId' => $_POST['referally_anon_id'],
'idempotencyKey' => 'signup:' . $user->id,
]);
foreach ($client->getReferralsEach(status: 'active') as $referral) { /* … */ }let client = ReferallyClient(apiKey: ProcessInfo.processInfo.environment["REFERALLY_API_KEY"])
let partner = try await client.postPartners(PostPartnersBody(email: "dana@example.com"))
for try await referral in client.getReferralsEach(status: "active") { print(referral.id) }val client = ReferallyClient(apiKey = System.getenv("REFERALLY_API_KEY"))
val partner = client.postPartners(PostPartnersBody(email = "dana@example.com"))
client.getReferralsEach(status = "active").forEach { println(it.id) }Errors are typed in every language; catch the specific class you care about:
- Python
- Go
- PHP
try:
client.get_partners_id("ptr_missing")
except NotFoundError as e:
print(e.code, e.request_id, e.docs_url)var notFound *referally.NotFoundError
if _, err := client.GetPartnersID(ctx, "ptr_missing"); errors.As(err, ¬Found) {
log.Println(notFound.Code, notFound.RequestID, notFound.DocsURL)
}try {
$client->getPartnersId('ptr_missing');
} catch (Referally\Exception\NotFoundError $e) {
echo $e->errorCode, $e->requestId, $e->docsUrl;
}Any operation can also be called by its id (client.call("getPartnersId", path=…) in Python, client.Call(ctx, "getPartnersId", …) in Go, and so on), and list_all / ListAll iterate any list operation.
Publishing
Each SDK is released by pushing a tag: py-v1.2.3, rb-v1.2.3, js-v1.2.3, php-v1.2.3, go-v1.2.3, swift-v1.2.3 or kt-v1.2.3 (.github/workflows/sdk-publish.yml). The generated layer is rebuilt with pnpm --filter @referally/sdk-js gen, and CI fails when any SDK is stale against the API.
TypeScript types for every endpoint
@referally/sdk-js/types has a type for every component in the OpenAPI document (Program, Partner, Referral, CommissionEvent, Settlement…). Each operation gets <Name>Body, <Name>Query and <Name>Response types, where the name is the method and path without /v1: PostReferralsAssociateBody, GetPartnersResponse. An operations table maps every operationId to its method and path:
import { operations, type PostReferralsAssociateBody, type AssociateResponse } from '@referally/sdk-js/types';
const body: PostReferralsAssociateBody = { programId: 'prg_...', customerRef: stripeCustomer.id };
const { method, path } = operations.postV1ReferralsAssociate;
const res = await fetch(`https://referalli.com${path}`, {
method,
headers: { authorization: `Bearer ${process.env.REFERALLY_API_KEY}`, 'content-type': 'application/json' },
body: JSON.stringify(body),
});
const { referral } = (await res.json()) as AssociateResponse;The types are generated from the API's own route schemas, and a test fails when they fall behind, so they match the server. Other generators (openapi-typescript, openapi-generator) work from /v1/openapi.json too.
Auth.js / NextAuth adapter
referallyAuthEvents returns a createUser handler for Auth.js. It hashes the new user's email, reads the referral context you stored, and calls associate with idempotency key authjs:{userId}. Failures never block signup.
import { ReferallyClient, referallyAuthEvents } from '@referally/sdk-js';
import { cookies } from 'next/headers';
const client = new ReferallyClient({
apiKey: process.env.REFERALLY_API_KEY!,
baseUrl: 'https://referalli.com',
});
const referallyEvents = referallyAuthEvents(client, {
programId: 'prg_...',
getReferral: () => JSON.parse(cookies().get('referally_ctx')?.value ?? '{}'),
});
// In your Auth.js config:
// events: { createUser: referallyEvents.createUser }referally_ctx is the first-party cookie the tracking script writes, so reading it on your server gives you anonId, clickId and code. The customerRef used is the Auth.js user id (or email when there is no id); see the note on matching billing ids.
Mobile trackers
ReferallyTracker for Swift and Kotlin persists an anonymous id on the device and calls the public visit and identify endpoints with your tracking token. It does not use an API key.
- Swift
- Kotlin
let tracker = ReferallyTracker(
trackingToken: "tk_...",
baseURL: URL(string: "https://referalli.com")!
)
tracker.trackVisit(code: deepLinkCode) // on app open from a referral deep link
tracker.identify(customerRef: user.id) // after signupval tracker = ReferallyTracker(
trackingToken = "tk_...",
baseUrl = "https://referalli.com",
storage = myPersistentStorage, // implements KeyValueStorage; the default is in-memory
)
tracker.trackVisit(code = deepLinkCode)
tracker.identify(customerRef = user.id)Send tracker.anonId to your backend at signup and pass it to associate there.
In-app purchases
Purchases create commissions, so they're reported from your server, never from the app: the app only holds the public tracking token. Send the App Store or Google Play receipt to your backend, validate it, then report the purchase with your API key. Each eventId is processed once.
- cURL
- Node
curl -X POST https://referalli.com/v1/mobile/purchases \
-H "Authorization: Bearer $REFERALLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"eventId":"GPA.1234-5678","customerRef":"user_42","amountMinor":999,"currency":"usd","productRef":"pro_monthly"}'import { ReferallyClient } from '@referally/sdk-js';
const referally = new ReferallyClient({ apiKey: process.env.REFERALLY_API_KEY!, baseUrl: 'https://referalli.com' });
await referally.trackMobilePurchase({ eventId: receipt.transactionId, customerRef: user.id, amountMinor: 999, currency: 'usd' });amountMinor is positive; for a refund send "type": "refund" with the refunded amount. The response has conversions, linesCreated and reversals. The old public POST /t/{token}/mobile/event endpoint answers 410 Gone.
MCP server
packages/mcp is a Model Context Protocol server that gives an AI assistant access to your program through your API key: read tools, plus write tools that only act after a confirmed second call (see CLI and MCP). It runs over stdio from a checkout of the repository:
REFERALLY_API_KEY=rk_... \
REFERALLY_API_URL=https://referalli.com \
pnpm --filter @referally/mcp startREFERALLY_API_URL defaults to http://localhost:4000; the server exits if REFERALLY_API_KEY is not set. Its read tools take no arguments:
Tool | Calls |
|---|---|
|
|
|
|
|
|
|
|
|
|