Skip to content

Search the documentation by title, section, or page text.

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#

ParameterTypeRequiredDescription
profileIdstringRequiredBotsi profile ID from Create a Profile

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.

FieldTypeWhat it is
profileIdstringBotsi profile ID
appUserIdstring or nullThe appUserId set on Create a Profile, if any
statestringThe Profile's subscription state. See below
totalRevenueUsdnumberLifetime revenue in USD
accessLevelsobjectEntitlements keyed by Access Level ID, the identifier set in the dashboard. For example, premium. Empty {} when the Profile has no entitlement
subscriptionsobjectThe same entitlement, keyed by store product ID. For example, premium_annual. Empty {} when there is none
nonSubscriptionsobjectPart of the contract, but always {} today. Nothing populates it yet
customarrayCustom attributes as { id, key, value } objects, the same form Add Custom Attributes returns. Get a Profile and Update a Profile only

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.

FieldTypeWhat it is
isActivebooleanWhether the entitlement currently grants access
storestring or nullapp_store, play_store, stripe, web2wave, or custom
sourceProductIdstring or nullStore product ID
sourceBasePlanIdstring or nullGoogle Play base plan ID
startsAtstring or nullISO 8601 timestamp
activatedAtstring or nullISO 8601 timestamp
renewedAtstring or nullISO 8601 timestamp
expiresAtstring or nullCalendar date only, as YYYY-MM-DD
willRenewboolean or nullWhether the subscription is set to renew
isLifetimeboolean or nullWhether the entitlement never expires
isRefundboolean or nullWhether the purchase was refunded
isInGracePeriodboolean or nullWhether the subscription is in a billing grace period
cancellationReasonstring or nullWhy the subscription was cancelled
unsubscribedAtstring or nullISO 8601 timestamp
billingIssueDetectedAtstring or nullISO 8601 timestamp
revokedAtstring or nullISO 8601 timestamp
offerIdstring or nullID of the offer applied to the purchase
activeIntroductoryOfferTypestring or nullType of the active introductory offer
activePromotionalOfferTypestring or nullType of the active promotional offer
activePromotionalOfferIdstring or nullID of the active promotional offer

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.

Errors#

CodeMessageCause
401No valid API key provided.The Authorization header is missing or holds an unknown key
404No profile found by id: fff3c120-0ca8-480f-9b19-3955a5428e39No Profile with that ID belongs to your app. A Profile owned by another app returns the same response