Get a Profile
GET https://api.botsi.com/v2/profiles/{profileId}Read a Profile's current access: its Access Levels, subscriptions, non-subscriptions, and custom attributes. Botsi answers from its own records and makes no call to Apple or Google, so this is cheap enough to call on every app launch.
This route accepts $BOTSI_SECRET_KEY or $BOTSI_PUBLIC_KEY in the Authorization header, with no Bearer prefix. The sample sends the secret key. From inside the app, send the public key. Delete a Profile is the exception and accepts the secret key only. See Authentication.
It's also the call to make when the store reports nothing to restore. See Restore Apple Store Purchases and Restore Google Play Subscriptions.
Path parameters#
There is no request body.
# Authorization: $BOTSI_SECRET_KEY or $BOTSI_PUBLIC_KEY
curl -X GET "https://api.botsi.com/v2/profiles/fff3c120-0ca8-480f-9b19-3955a5428e39" \
-H "Authorization: $BOTSI_SECRET_KEY"Response#
{
"ok": true,
"data": {
"profileId": "fff3c120-0ca8-480f-9b19-3955a5428e39",
"appUserId": "user-123",
"state": "subscribed",
"totalRevenueUsd": 49.99,
"accessLevels": {
"premium": {
"isActive": true,
"store": "app_store",
"sourceProductId": "premium_annual",
"sourceBasePlanId": null,
"startsAt": "2026-10-07T14:03:11.000Z",
"activatedAt": "2026-10-07T14:03:11.000Z",
"renewedAt": null,
"expiresAt": "2027-10-07",
"willRenew": true,
"isLifetime": false,
"isRefund": false,
"isInGracePeriod": false,
"cancellationReason": null,
"unsubscribedAt": null,
"billingIssueDetectedAt": null,
"revokedAt": null,
"offerId": null,
"activeIntroductoryOfferType": null,
"activePromotionalOfferType": null,
"activePromotionalOfferId": null
}
},
"subscriptions": {
"premium_annual": {
"isActive": true,
"store": "app_store",
"sourceProductId": "premium_annual",
"sourceBasePlanId": null,
"startsAt": "2026-10-07T14:03:11.000Z",
"activatedAt": "2026-10-07T14:03:11.000Z",
"renewedAt": null,
"expiresAt": "2027-10-07",
"willRenew": true,
"isLifetime": false,
"isRefund": false,
"isInGracePeriod": false,
"cancellationReason": null,
"unsubscribedAt": null,
"billingIssueDetectedAt": null,
"revokedAt": null,
"offerId": null,
"activeIntroductoryOfferType": null,
"activePromotionalOfferType": null,
"activePromotionalOfferId": null
}
},
"nonSubscriptions": {},
"custom": [
{ "id": "Kj3xQz9a", "key": "user_level", "value": "vip" }
]
}
}Profile fields#
This is the same Profile object that Validate Apple Store Purchase, Validate Google Play Store Purchase, and both Restore endpoints return. Only Get a Profile and Update a Profile add custom.
state is one of subscribed, active-trial, auto-renew-off, subscription-cancelled, trial-cancelled, never-subscribed, billing-issue, grace-period, trial-expired, subscription-expired, or refunded.
A Profile carries at most one entitlement today, and it appears twice: once in accessLevels and once in subscriptions. For example, the Profile above holds the premium Access Level through the premium_annual product, so the same entitlement sits under accessLevels.premium and subscriptions.premium_annual. Read state and accessLevels to decide what the user has access to.
Entitlement fields#
Each value inside accessLevels and subscriptions is an entitlement object.
Parse expiresAt as a date, not a timestamp
Every other date field is a full ISO 8601 timestamp. expiresAt is a calendar date with no time or time zone, for example 2027-10-07. A parser that expects a timestamp will reject it or read it as midnight UTC.
The entitlement also carries id (an internal row ID), sourcePurchaseToken (Google Play only), and createdDate (an ISO 8601 timestamp). They're internal, so the example above leaves them out, but they're in every response.