What the API provides
The Rewarded API lets an app:
register and authenticate rewarded users;
show offers that are available to the signed-in user;
track genuine offer impressions and clicks;
show event-level progress and earned rewards;
display monetary and points balances;
request and track withdrawals when payouts are enabled;
manage the user's profile, devices, password, and account deletion;
provide in-app support tickets; and
optionally support Google, Facebook, and Apple sign-in.
ℹ️ Offer completion is not reported by the app. Swaarm records progress after it receives and accepts an advertiser postback. The app reads that server-side state from the API.
Before you start
Ask Swaarm contact:
the GraphQL endpoint for your tenant;
confirmation that Rewarded is enabled;
a test offer and access to it for your rewarded users;
the tenant's monetary currency and points convention;
the minimum withdrawal and payment-provider configuration, if payouts are required;
email-verification and password-reset configuration;
social-login client IDs, if social login is required; and
app branding and deep-link settings.
All GraphQL operations use one tenant-specific endpoint:
POST <GRAPHQL_ENDPOINT> Content-Type: application/json
The request body contains a query and, normally, variables:
{ "query": "query CurrentUser { rewardedUser { id email nickname } }", "variables": {} }Use a GraphQL client such as Apollo, urql, Apollo iOS, Apollo Kotlin, or graphql-flutter when practical. Introspection and code generation help catch schema changes before release.
Check feature availability
ℹ️ Schemas can differ between tenants and releases. During development, enable schema introspection and generate client types from the target tenant. For a quick check:
query RewardedApiCapabilities { queryType: __type(name: "Query") { fields { name } } mutationType: __type(name: "Mutation") { fields { name } } rewardedUserType: __type(name: "RewardedUser") { fields { name } } }⚠️ Do not run a client against operations that are absent from its generated schema. Introspection may be disabled in hardened production environments, so perform this check in development or CI.
A small HTTP client
If you are not using a GraphQL library, this TypeScript helper handles both HTTP failures and GraphQL errors:
type GraphQLError = { message: string; path?: Array<string | number>; extensions?: Record<string, unknown>; }; type GraphQLResponse<T> = { data?: T; errors?: GraphQLError[]; }; async function graphql<T>( query: string, variables: Record<string, unknown> = {}, accessToken?: string, ): Promise<T> { const response = await fetch(GRAPHQL_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, body: JSON.stringify({ query, variables }), }); const payload = (await response.json()) as GraphQLResponse<T>; if (!response.ok || payload.errors?.length || !payload.data) { throw new Error( payload.errors?.map((error) => error.message).join("; ") || `GraphQL request failed with HTTP ${response.status}`, ); } return payload.data; }⚠️ GraphQL can return HTTP 200 with an errors array and partial data. Always inspect both.
Registration, verification, and authentication
Register with email and password
createRewardedUser is public. Use variables so credentials are not embedded in query text or logs.
mutation Register($input: RewardedUserInput!) { createRewardedUser(input: $input) { id email nickname status } }{ "input": { "email": "user@example.com", "password": "a-long-user-chosen-password", "nickname": "GamerPro", "firstName": "Sam", "lastName": "Lee", "country": "US", "status": "PENDING", "internalId": "your-app-user-123", "customFields": [ { "name": "preferred_category", "value": "gaming" } ] } }Important registration rules:
emailandpasswordare required.Email must be unique; a duplicate registration returns a GraphQL error.
Use an ISO 3166-1 alpha-2 country code such as
US,DE, orRO.Use
PENDINGfor an email-verification flow. In newer schemasstatuscan be omitted and defaults toPENDING; passing it keeps the request compatible with older schemas where it is required.internalIdis an optional identifier from your own system. Do not use the Swaarm user ID and your internal ID interchangeably.referralId, when used, is the Swaarmidof the referring rewarded user.Do not pass
publisherIdunless Swaarm has explicitly assigned one for this integration.Never place secrets, authentication tokens, or regulated personal data in
customFields.
Verify the email address
If your tenant uses code-based verification, first trigger the email using the user ID returned by registration:
mutation SendVerificationEmail($userId: String!) { rewardedTriggerVerificationEmail(id: $userId) }Then submit the email and the code entered by the user:
mutation VerifyEmail($email: String!, $code: String!) { rewardedVerifyEmail(email: $email, code: $code) }A successful verification returns true and activates the user. Some tenants instead use an emailed verification link that returns to an app deep link. Confirm the desired flow during onboarding.
Log in
mutation Login($input: AuthLoginInput!) { login(input: $input) { authChallenge token { accessToken refreshToken tokenType expiresIn } } }{ "input": { "username": "user@example.com", "password": "a-long-user-chosen-password" } }For authenticated operations, send:
Authorization: Bearer <accessToken>
expiresIn is the access-token lifetime in seconds. Do not hard-code a lifetime in the app. Schedule renewal from the returned value and also recover from an HTTP 401.
Store tokens in the platform's secure storage, such as iOS Keychain or Android Keystore-backed storage. Never log tokens, include them in URLs, or put them in analytics events.
Refresh the access token
mutation RefreshAccessToken($input: AuthRefreshTokenInput!) { refreshToken(input: $input) { authChallenge token { accessToken refreshToken tokenType expiresIn } } }{ "input": { "refreshToken": "<refresh-token>" } }Replace the stored access token with the returned one. If the response contains a refresh token, replace that atomically as well. If refresh fails, clear the session and ask the user to log in again.
authChallenge is normally NONE. A tenant can also return challenges such as NEW_PASSWORD_REQUIRED or ACCOUNT_MIGRATION_REQUIRED; do not assume token is present until the challenge has been completed.
Optional social login
When configured for the tenant, rewardedSocialLogin verifies a provider token and either signs in the existing active user or creates a new active rewarded user.
mutation SocialLogin($input: RewardedSocialLoginInput!) { rewardedSocialLogin(input: $input) { authChallenge isNewUser token { accessToken refreshToken tokenType expiresIn } } }Recommended inputs:
{ "input": { "provider": "GOOGLE", "idToken": "<google-id-token>" } }{ "input": { "provider": "FACEBOOK", "accessToken": "<facebook-access-token>" } }{ "input": { "provider": "APPLE", "idToken": "<apple-id-token>", "firstName": "Sam", "lastName": "Lee" } }ℹ️ For Apple, pass the name on the first authorization because Apple may not include it on later sign-ins. Social login requires tenant-side client ID configuration and an email from the provider.
Password reset
Request a reset code:
mutation RequestPasswordReset($email: String!) { rewardedRequestPasswordReset(email: $email) }Submit the code and a new password:
mutation ResetPassword( $email: String! $newPassword: String! $code: String! ) { rewardedResetPassword( email: $email newPassword: $newPassword code: $code ) }ℹ️ Reset codes expire. Show a generic confirmation after requesting a code and rate-limit the UI to avoid account enumeration and abuse.
Load the current user and app shell
The app-facing user query is rewardedUser; it resolves from the bearer token. Admin-only operations such as singleRewardedUser are not needed in an end-user app.
query AppBootstrap { rewardedUser { id internalId email nickname firstName lastName country status profilePictureUrl customFields { name value } stats { startedOffers completedOffers totalTheyGet totalTheyGetPoints } balance { availableBalance pendingBalance withdrawnBalance } } }ℹ️ profilePictureUrl and withdrawnBalance are newer fields. Omit them if they are absent from the target tenant's generated schema.
The monetary fields (theyGet, totalTheyGet, and balances) are amounts in the tenant's configured platform currency. theyGetPoints fields are the corresponding point values. Ask Swaarm how your tenant displays and converts them; do not assume that one point equals one currency unit.
Handle user status explicitly:
Status | Recommended app behaviour |
| Continue the tenant's email-verification or approval flow; do not show the offer wall yet. |
| Allow normal Rewarded API use. |
| End the session and show that the account is unavailable. |
| End the session and show the tenant-approved support or appeal message. This status exists only in newer schemas. |
Optional public app branding
An app can load tenant branding before login when rewardedAppDetails is available:
mutation AppDetails { rewardedAppDetails { title logo playStoreUrl iosStoreUrl webUrl } }ℹ️ This is currently a mutation even though it reads configuration. Treat nullable store and web URLs as optional.
Update the user's profile
mutation UpdateProfile($input: RewardedUserUpdateInput!) { updateRewardedUser(input: $input) { id nickname firstName lastName country profilePictureUrl customFields { name value } } }The update input is a full desired profile, not a reliable patch: omitted nullable fields can be stored as null, and the custom-field list is replaced. Load the current profile, merge edits locally, and send all values that must be preserved.
The self-service mutation updates profile fields. User status and internal notes are controlled by Swaarm even though shared schema inputs may expose those names.
Record an app open and device details
When supported, call rewardedAppOpen after authentication and whenever important device or push-token data changes:
mutation RecordAppOpen($input: RewardUserOpenEvent!) { rewardedAppOpen(input: $input) }{ "input": { "time": "2026-08-05T12:30:00", "deviceId": "<stable-app-device-id>", "deviceType": "PHONE", "os": "IOS", "osv": "18.5", "locale": "en-US", "timezone": "Europe/Bucharest", "appVersion": "1.4.0", "buildVersion": "104", "expoPushToken": "<optional-expo-push-token>", "trackingPermission": "DENIED" } }Only send data for which your app has a legitimate purpose and the required user consent. Use a stable app-scoped device ID, not a fingerprint assembled from unrelated device attributes. expoPushToken is only relevant to tenants using Expo push notifications.
Build the offer wall
Query available offers
query OfferWall( $filter: RewardedOffersFilter $pagination: Pagination! ) { rewardedUser { offers( filter: $filter pagination: $pagination ordering: DEFAULT ) { totalCount edges { node { id name description appStoreId previewUrl trackingUrl impressionUrl leadflow kpi additionalInformation tags verticals restrictions percentageCompleted completedStatus totalPayout { theyGet theyGetPoints } targeting { countries os { platform version { minVersion maxVersion } } } eventTypes { id name description category completed payout { theyGet theyGetPoints } reward { theyGet theyGetPoints time } creativeUrl creativeThumbnail } creatives { id title description creatives { id title type url thumbnailUrl active hidden } } } } } } }{ "filter": { "countries": ["US"], "oses": ["IOS"], "completionStatuses": ["UNATTEMPTED", "STARTED"] }, "pagination": { "offset": 0, "limit": 20 } }ℹ️ Fields added by newer Rewarded releases - such as verticals, restrictions, category, and the plural filters - should be omitted for an older tenant schema.
Filtering behavior
RewardedOffersFilter can include:
Field | Behaviour |
| Uses request IP and user agent to infer country and OS. |
| Removes fully completed offers. |
| Includes offers targeting the given country or any country in the list. |
| Excludes offers matching the supplied countries. |
| Includes offers for the given platform, normally |
| Excludes the supplied platforms. |
| Searches offer ID, name, and description. |
| Includes |
| Includes or excludes offers by tag. |
| Includes offers having any supplied vertical. |
| Includes offers having any supplied restriction. |
ℹ️ For native apps, explicit country and OS filters are more predictable than autoFilter, because an HTTP library's user agent may not identify the device OS. Use values supplied or verified by your application and apply your own consent and location policies.
Filters are combined, so an offer must pass every populated category of filter. Values within a list generally use “any match” semantics.
Ordering behavior
Newer schemas accept ordering:
DEFAULTsorts by recent EPC and can include unattempted, started, and completed offers according to the filter.TAG_PRIORITIZEDplaces specially tagged offers first and hides offers on which the user already has progress.
ℹ️ The schema default can be TAG_PRIORITIZED. Pass DEFAULT explicitly for a general offer wall or when you need started offers to remain visible.
Highlighted and single-offer screens
highlightedOffers returns a small tenant-curated set:
query HighlightedOffers { rewardedUser { highlightedOffers { edges { node { id name totalPayout { theyGet theyGetPoints } creatives { creatives { type url thumbnailUrl active hidden } } } } } } }In newer schemas, load a signed-in user's offer detail with the nested field:
query OfferDetails($id: String!) { rewardedUser { offer(id: $id) { id name description trackingUrl impressionUrl completedStatus percentageCompleted eventTypes { id name description completed payout { theyGet theyGetPoints } reward { theyGet theyGetPoints time } } } } }Use the root rewardedOffer(id: ...) only where the nested field is not yet available.
Pagination
Offer, reward, and progress connections use offset pagination:
input Pagination { offset: Int limit: Int }Start with { offset: 0, limit: 20 }, then increase offset by the number of returned edges. Stop when no edges are returned or when the accumulated result reaches totalCount. Data can change between requests, so deduplicate by stable IDs when merging pages.
Tracking impressions and clicks correctly
Every returned offer has user-specific tracking URLs. Do not construct these URLs yourself or copy a URL from one user to another.
Impression
When an offer is genuinely displayed, make a GET request to its impressionUrl if the value is not null or empty. Preserve the URL exactly. Avoid firing impressions for prefetched items that never become visible, and avoid repeated calls caused only by UI re-rendering.
Click
When the user chooses the offer, open its trackingUrl exactly as returned. Do not strip, reorder, or replace query parameters. The URL carries the Swaarm attribution context for that user and offer.
Treat tracking URLs as sensitive operational data: they can contain user and attribution identifiers. Do not write the full URL to analytics or crash logs.
Completion
⚠️ Do not mark an event complete or credit a balance because the user clicked, installed an app, or returned to your app. Swaarm records progress only after an advertiser postback passes evaluation. Refresh the offer detail or progress query when the app resumes and poll conservatively if the UI needs near-real-time updates.
ℹ️ Postbacks can arrive later than the user's return to the app. Show a neutral “tracking” or “pending confirmation” state rather than promising an immediate reward.
Show progress, reward history, and analytics
There are three complementary views of progress.
Progress on an offer
Use these fields on RewardedOffer and RewardedEventType:
completedStatus:UNATTEMPTED,STARTED, orCOMPLETED;percentageCompleted: a fraction from 0 to 1;eventTypes[].completed: whether the event has completed;eventTypes[].payout: the expected reward for the event; andeventTypes[].reward: the actual credited amount and time, or null before completion.
⚠️ Do not calculate the user's credited reward from payout; use reward, progress, or rewards. Payout configuration can change, while credited progress stores the actual award.
Chronological event history
Newer schemas expose passed progress entries sorted newest first:
query RewardProgress( $filter: RewardedUserProgressFilter $pagination: Pagination! ) { rewardedUser { progress(filter: $filter, pagination: $pagination) { totalCount edges { node { offer { id name } eventType { id name } reachedAt theyGet theyGetPoints } } } } }{ "filter": { "offerId": "12345", "from": "2026-07-01T00:00:00", "to": "2026-08-05T23:59:59" }, "pagination": { "offset": 0, "limit": 50 } }Rewards grouped by offer
query Rewards($pagination: Pagination!) { rewardedUser { rewards(pagination: $pagination) { totalCount edges { node { offer { id name } totalPayout { theyGet theyGetPoints } completed { eventType { id name description } theyGet theyGetPoints time } } } } } }ℹ️ Older schemas do not accept a filter argument on rewards and may not expose totalPayout. The completed event field is named eventType, not event.
Time-series report
query RewardReport($input: RewardedReportInput!) { rewardedUser { report(input: $input) { start end granularity rows { dimensions { dimension value } theyGet theyGetPoints } } } }{ "input": { "start": "2026-07-01T00:00:00", "end": "2026-08-01T00:00:00", "granularity": "DAY", "dimensions": ["DATETIME", "OFFER_ID"] } }Supported granularities are DAY, WEEK, MONTH, YEAR, and ALL. Dimensions are DATETIME and OFFER_ID. Use ISO-8601 local date-time strings without a UTC suffix unless your tenant confirms another convention, and agree on the reporting timezone with Swaarm.
Balances and withdrawals
Payout operations are available only when the Rewarded payment flow is deployed and a provider is configured for the tenant.
Read the balance and payment history
query Wallet($first: Int, $offset: Int) { rewardedUser { id balance { availableBalance pendingBalance withdrawnBalance } payments(first: $first, offset: $offset) { totalCount edges { node { id createdAt amount start end status publisherNote paymentProcessorResponse { key value } } } } } }availableBalancecan be requested for withdrawal.pendingBalanceis reserved by requests still awaiting a decision.withdrawnBalanceis the amount recorded as paid.
ℹ️ The server is authoritative. Refresh the wallet immediately before enabling the withdrawal action.
Request a withdrawal
The correct mutation is requestRewardedUserPayment:
mutation RequestWithdrawal($input: PartnerPublisherPaymentInput!) { requestRewardedUserPayment(input: $input) { id createdAt amount start end status publisherNote paymentProcessorResponse { key value } } }{ "input": { "rewardedUserId": "<current-rewarded-user-id>", "amount": 25.0, "start": "2026-08-05T12:30:00", "end": "2026-08-05T12:30:00", "publisherNote": "Requested from iOS app" } }The current schema requires start and end, although the Rewarded implementation assigns the request period on the server. Send the current local date-time for both unless your Swaarm integration engineer specifies another convention. Do not send publisherId unless instructed.
The server enforces:
an authenticated user can request only their own balance;
the amount must be greater than zero;
the amount must meet the tenant's configured minimum;
the amount cannot exceed
availableBalance; andconcurrent requests for the same user are serialised.
ℹ️ Creating a request does not guarantee payment. It creates a record in REQUESTED state for review.
⚠️ Do not automatically retry this mutation after a timeout. There is no client idempotency key. First reload payment history and reconcile the result; retry only if no request was created.
Payment states and claim links
Possible states are:
Status | Meaning for the app |
| Created by the user and awaiting review. |
| Accepted for internal processing. |
| Approved; a provider-specific claim link may now be available. |
| Sent to a configured payment processor. |
| Recorded as paid. |
| Rejected; the reserved amount becomes available again. |
Treat paymentProcessorResponse as provider-specific key/value data. If it contains a non-empty url, present a “Claim reward” action and open the URL in the system browser:
const claimUrl = payment.paymentProcessorResponse?.find( (entry) => entry?.key === "url", )?.value; if (claimUrl) { await openInSystemBrowser(claimUrl); }⚠️ Do not require only PAID before showing a link: some configured providers create the link when the request becomes APPROVED. Validate that the URL uses HTTPS and do not log it.
For tenants without the nested payments field, the equivalent root query is:
query PaymentHistory($userId: String!, $first: Int, $offset: Int) { rewardedUserPayments(userId: $userId, first: $first, offset: $offset) { totalCount edges { node { id createdAt amount status paymentProcessorResponse { key value } } } } }In-app support
When enabled, support tickets can be loaded through the current user:
query MySupportTickets($first: Int!, $offset: Int!) { rewardedUser { supportTickets(first: $first, offset: $offset) { totalCount edges { node { id title body status lastMessagedAt messages { id body time user { id nickname } accountManager { id } } } } } } }Create a ticket with the signed-in user's Swaarm ID:
mutation CreateSupportTicket($input: RewardedUserSupportTicketInput!) { rewardedCreateSupportTicket(ticket: $input) { id title status lastMessagedAt } }{ "input": { "title": "Missing reward", "rewardedUserId": "<current-rewarded-user-id>", "message": { "body": "I completed the offer but do not see the final event yet." } } }Reply to an existing ticket:
mutation ReplyToSupportTicket( $ticketId: String! $message: RewardedUserSupportTicketMessageInput! ) { rewardedCreateSupportTicketMessage( ticketId: $ticketId message: $message ) { id status lastMessagedAt messages { id body time } } }The server verifies that the authenticated user owns the ticket. Do not use the root rewardedSupportTickets list in an end-user app; it is an admin operation.
Account deletion
When the user confirms deletion, call the authenticated operation:
mutation DeleteMyRewardedAccount { rewardedRequestDeletion }On success, clear local tokens and user data. The operation disables the account, removes authentication access, and anonymizes profile fields. Treat it as irreversible from the app and require a clear confirmation step.
Error handling and safe retries
Handle these categories separately:
HTTP 401: refresh once, replay a safe query, or return to login if refresh fails.
HTTP 4xx/5xx: record a sanitized request ID and show a retryable or permanent error as appropriate.
GraphQL
errors: inspect the message and path even when HTTP status is 200.Network timeout: retry idempotent queries with exponential backoff and jitter.
Mutation timeout: reconcile server state before retrying registration, withdrawal, support-ticket creation, or account deletion.
Partial data: use it only if your UI explicitly supports partial results; otherwise fail the screen as one operation.
⚠️ Never expose raw stack traces or internal error details to users. Never log passwords, tokens, complete tracking URLs, claim URLs, social-provider tokens, or full GraphQL variables containing personal data.
Recommended app architecture
Keep API concerns behind a small application service layer:
UI screens -> auth/session store -> rewarded API client -> generated GraphQL operations and types -> secure token storage
Useful client-side modules are:
SessionService: login, refresh, logout, and challenge handling;ProfileService: bootstrap, update, device registration, and deletion;OfferService: offer wall, detail, impression, and click handling;RewardService: progress, rewards, and report refresh;WalletService: balance, payment history, withdrawal reconciliation, and claim links; andSupportService: tickets and replies.
⚠️ Cache display data only for a short time. Never treat cached offer payout, progress, balance, or payment status as authoritative.
Production checklist
Before release, verify all of the following against the production tenant:
GraphQL operations are generated from the production-compatible schema.
Registration, duplicate email, verification, login, refresh, logout, and password reset work.
Tokens are stored securely and removed on logout or deletion.
PENDING,ACTIVE,INACTIVE, and fraud-blocked users receive the intended UX.Country and OS filtering return the expected test offers.
One visible card produces the expected impression request.
A click opens the exact returned tracking URL.
A test advertiser postback produces event progress and the correct credited amount.
Delayed postbacks do not cause the app to award rewards locally.
Pagination, empty states, and removed offers are handled.
Monetary amounts use the correct tenant currency and formatting.
Withdrawal minimum, insufficient balance, denial, approval, claim link, and reconciliation are tested.
Support-ticket access cannot cross user boundaries.
Logs and analytics contain no credentials or sensitive tracking and claim URLs.
Deep links and social-login provider settings match the released app identifiers.
Consent, privacy notice, account deletion, and device-data collection meet the app's legal requirements.
Troubleshooting
The API returns “Access denied”
Confirm Rewarded is enabled for the tenant, the access token is current, and the operation is app-facing rather than admin-only. A user can read or mutate only their own Rewarded data.
No offers are returned
Check offer access and status in Swaarm, then remove filters one at a time. Pass ordering: DEFAULT if the user has already started offers. For native apps, test explicit countries and oses rather than relying on autoFilter.
A click is recorded but progress does not update
A click is not completion. Confirm that the advertiser sent the correct postback, that it passed Swaarm evaluation, and that the offer/event/user attribution is correct. Allow for postback delay before escalating.
The displayed amount differs from the offer payout
Expected payout and credited reward are different concepts. Display credited progress for the wallet and history. Confirm tenant currency, event visibility, and points configuration with Swaarm.
A withdrawal fails
Reload availableBalance, confirm the configured minimum, and look for an existing request created during a previous timeout. If the server says another request is in progress, wait briefly and reconcile payment history rather than submitting repeatedly.
For integration support, contact your Swaarm account manager or support@swaarm.com and provide the tenant, operation name, sanitized timestamp, and a request/correlation ID. Do not send passwords, bearer tokens, provider tokens, or full tracking URLs.
