Skip to main content

Users

User management

User registration by email​

Anonymous, per-promotion registration: creates (or reuses) a user tied to UserId (the user's email) and this PromotionId. No password is involved — this is the lightweight identity used by the rest of the participation endpoints (beginparticipation, consumerprofile, etc, passing the same UserId).

POST /registeruser
REQUEST
{
"PromotionId": "{Promotion id}",
"UserId": "{user email}",
"Trafficsource": "{traffic source or UTM value}",
"FullName": "{Optional - Name of user}",
"Nickname": "{Optional - Name to show in profile and rankings}",
"City": "{Optional - City from where the user participates}",
"DateOfBirth": "{Optional - user date of birth in format yyyy-mm-dd}",
"AcceptedTermsAndConditions": true,
"MarketingOptIn": true,
"BrandOwnerOptin": true
}

AcceptedTermsAndConditions/MarketingOptIn/BrandOwnerOptin are only relevant when this promotion is issy-managed — see Terms & Conditions Acceptance. Omit MarketingOptIn and BrandOwnerOptin entirely if you don't know the tenant's mode; only send whichever one applies.

If the promotion has the Age Gate enabled (requireAgegateConfiguration: true and minimumAge > 0), DateOfBirth becomes effectively required unless the user already has a birth date stored. A stored birth date always wins and is never overwritten — DateOfBirth is only used (and saved) when the user has none. The check runs before the user/profile is created or updated.

RESPONSE (200)
{
"success": true
}

Error responses:

HTTPerrorCodeCause
404PROMOTION_NOT_FOUNDPromotionId doesn't exist for this tenant.
412TERMS_NOT_ACCEPTEDPromotion is issy-managed and required acceptance is missing — see requiredAcceptances.
400TERMS_FIELD_NOT_APPLICABLESent BrandOwnerOptin/MarketingOptIn for the wrong tenant mode.
400INVALID_DATE_OF_BIRTHDateOfBirth was sent but isn't a real past date in yyyy-MM-dd format (or is more than 120 years ago), and it would be stored: the account is new, or it has no birth date yet. Ignored for users who already have one.
406BIRTHDATE_REQUIREDThe promotion has the Age Gate enabled and no birth date is available (neither one already stored for the user nor DateOfBirth in the body). The user and profile are not created.
406AGE_REQUIREMENT_NOT_METThe promotion has the Age Gate enabled and the user is younger than minimumAge. The user and profile are not created.
401 / 406INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500INTERNAL_ERRORUnexpected server error.
error response example
{
"success": false,
"errorCode": "TERMS_NOT_ACCEPTED",
"message": "The user must accept the Terms & Conditions / Privacy Policy before continuing.",
"requiredAcceptances": ["AcceptedTermsAndConditions"]
}

User registration by phone​

Same as email registration, but UserId is the user's phone number. Email here is only a reference field (not used as the identifier).

POST /registeruserbyphone
REQUEST
{
"PromotionId": "{Promotion id}",
"UserId": "{user phone number}",
"Trafficsource": "{traffic source or UTM value}",
"FullName": "{Optional - Name of user}",
"Nickname": "{Optional - Name to show in profile and rankings}",
"City": "{Optional - City from where the user participates}",
"DateOfBirth": "{Optional - user date of birth in format yyyy-mm-dd}",
"Email": "{Optional - reference email}",
"AcceptedTermsAndConditions": true,
"MarketingOptIn": true,
"BrandOwnerOptin": true
}
RESPONSE (200)
{
"success": true
}

Same error table as User registration by email above.

Account Authentication​

These endpoints manage a full user account (email/password, verification code, or social login), as an alternative to the anonymous email/phone registration above. On success they return the account's id and a sessionId. Use id as UserId in the rest of the endpoints, the same way the anonymous registration flow works. sessionId is only required for issy-managed loyalty programs — see User Session in Get Started; for client-managed promotions it can be ignored.

All account endpoints below still require the standard issy-token + issy-tenant-id headers described in Get Started.

Register with email and password​

POST /auth/register
REQUEST
{
"Email": "{user email}",
"Password": "{user password}",
"FirstName": "{Optional - user first name}",
"LastName": "{Optional - user last name}",
"Nickname": "{Optional - Name to show in profile and rankings}",
"City": "{Optional - City from where the user participates}",
"CountryCode": "{Optional - user country code}",
"DateOfBirth": "{Optional - user date of birth in format yyyy-mm-dd (yyyy-01-01 for year-of-birth-only age gates). Only stored if the user has none yet}",
"PromotionId": "{Optional - Promotion id, needed only to enforce Terms & Conditions acceptance and the Age Gate}",
"AcceptedTermsAndConditions": true,
"MarketingOptIn": true,
"BrandOwnerOptin": true
}

If PromotionId is omitted, the account is created without any T&C check at all. If it's sent, the Terms & Conditions Acceptance gate applies exactly like the other gated endpoints.

If PromotionId is sent and that promotion has the Age Gate enabled (requireAgegateConfiguration: true and minimumAge > 0), DateOfBirth is required and validated before the account is created.

RESPONSE (200)
{
"success": true,
"sessionId": "{use this for issy-managed loyalty endpoints, see User Session in Get Started}",
"user": {
"id": "{user identifier, use this value as UserId}",
"userName": "{user email}",
"firstName": "{user first name}",
"lastName": "{user last name}",
"email": "{user email}",
"phone": "{user phone}",
"city": "{user city}",
"nickname": "{user nickname}",
"countryCode": "{user country code}"
}
}

Error responses:

HTTPerrorCodeCause
400EMAIL_PASSWORD_REQUIREDMissing Email/Password.
400USER_ALREADY_EXISTSA user with that email already exists.
400USER_CREATION_FAILEDASP.NET Identity rejected the account (e.g. weak password) — message has the details.
412TERMS_NOT_ACCEPTEDOnly if PromotionId was sent and the gate applies.
400TERMS_FIELD_NOT_APPLICABLEOnly if PromotionId was sent and the wrong opt-in field for the tenant's mode was included.
400INVALID_DATE_OF_BIRTHDateOfBirth was sent but isn't a real past date in yyyy-MM-dd format (or is more than 120 years ago), and it would be stored: the account is new, or it has no birth date yet. Ignored for users who already have one.
406BIRTHDATE_REQUIREDOnly if PromotionId was sent and the promotion has the Age Gate enabled: the user has no birth date stored and DateOfBirth wasn't sent.
406AGE_REQUIREMENT_NOT_METOnly if PromotionId was sent and the promotion has the Age Gate enabled: the user is younger than minimumAge.
401INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500INTERNAL_ERRORUnexpected server error.
error response example
{
"success": false,
"errorCode": "USER_ALREADY_EXISTS",
"message": "A user with this email already exists."
}

Register with verification code​

Two-step alternative to Register with email and password that requires proving ownership of the email or phone before the account is created — no account exists until the code is confirmed, so there's no unverified/"ghost" account left behind if the user never completes step 2.

Step 1 — request a code

POST /auth/register/request-code
REQUEST
{
"Email": "{user email - send this OR PhoneNumber, not both}",
"PhoneNumber": "{user phone number - alternative to Email}",
"PromotionId": "{Optional - if the promotion has a custom SMS message configured, it's used instead of the generic one. No effect on email codes.}"
}

Sends a 6-digit code by SMS (if PhoneNumber) or email (if Email). Rate limited per identifier: max 3 requests / 10 minutes, 5 / hour.

RESPONSE (200)
{
"success": true,
"tokenId": "{opaque id, pass it back to /auth/register/confirm}",
"channel": "{Email or Phone - where the code was sent}",
"expiresInSeconds": 600
}

Error responses:

HTTPerrorCodeCause
400EMAIL_OR_PHONE_REQUIREDNeither or both of Email/PhoneNumber were sent.
400ALREADY_REGISTEREDThat email/phone already has a full account — use Login instead.
429TOO_MANY_REQUESTSRate limit exceeded.
401INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500INTERNAL_ERRORUnexpected server error.

Step 2 — confirm the code and complete registration

POST /auth/register/confirm
REQUEST
{
"TokenId": "{tokenId from the request-code response}",
"Code": "{6-digit code the user received}",
"Email": "{must match what was sent to request-code, if Email was the channel}",
"PhoneNumber": "{must match what was sent to request-code, if PhoneNumber was the channel}",
"Password": "{user password}",
"FirstName": "{Optional - user first name}",
"LastName": "{Optional - user last name}",
"Nickname": "{Optional - Name to show in profile and rankings}",
"City": "{Optional - City from where the user participates}",
"CountryCode": "{Optional - user country code}",
"DateOfBirth": "{Optional - user date of birth in format yyyy-mm-dd}",
"PromotionId": "{Optional - associates the account with this promotion; also needed to enforce Terms & Conditions and the Age Gate, same as Register with email and password}",
"SourceCode": "{Optional - attribution code (traffic source or promotion code), stored as-is, not validated}",
"AcceptedTermsAndConditions": true,
"MarketingOptIn": true,
"BrandOwnerOptin": true
}

The password is only requested here, after the code is verified — step 1 never asks for it.

RESPONSE (200)
{
"success": true,
"sessionId": "{use this for issy-managed loyalty endpoints, see User Session in Get Started}",
"user": {
"id": "{user identifier, use this value as UserId}",
"userName": "{user email}",
"firstName": "{user first name}",
"lastName": "{user last name}",
"email": "{user email}",
"phone": "{user phone}",
"city": "{user city}",
"nickname": "{user nickname}",
"countryCode": "{user country code}"
}
}

Error responses:

HTTPerrorCodeCause
404CODE_NOT_FOUNDTokenId doesn't match a pending code.
401CODE_INVALIDCode doesn't match.
401CODE_EXPIREDThe code expired (10 minutes after request-code).
401CODE_ALREADY_USEDThe code was already used to complete a registration.
400EMAIL_PASSWORD_REQUIREDMissing Password, or the Email/PhoneNumber doesn't match the one verified in step 1.
400USER_ALREADY_EXISTSAn account for that email/phone already exists (race with another registration).
400USER_CREATION_FAILEDASP.NET Identity rejected the account (e.g. weak password) — message has the details.
412TERMS_NOT_ACCEPTEDOnly if PromotionId was sent and the gate applies.
400TERMS_FIELD_NOT_APPLICABLEOnly if PromotionId was sent and the wrong opt-in field for the tenant's mode was included.
400INVALID_DATE_OF_BIRTHDateOfBirth was sent but isn't a real past date in yyyy-MM-dd format (or is more than 120 years ago), and it would be stored: the account is new, or it has no birth date yet. Ignored for users who already have one.
406BIRTHDATE_REQUIREDOnly if PromotionId was sent and the promotion has the Age Gate enabled: no birth date is available (neither one stored on the existing guest profile being upgraded nor DateOfBirth).
406AGE_REQUIREMENT_NOT_METOnly if PromotionId was sent and the promotion has the Age Gate enabled: the user is younger than minimumAge.
401INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500INTERNAL_ERRORUnexpected server error.

Login with email and password​

POST /auth/login
REQUEST
{
"Email": "{user email - send this OR PhoneNumber}",
"PhoneNumber": "{user phone number - alternative to Email}",
"Password": "{user password}",
"PromotionId": "{Optional - Promotion id. When sent, the response includes a termsAcceptance block for that promotion and the promotion's Age Gate is enforced}",
"DateOfBirth": "{Optional - user date of birth in format yyyy-mm-dd (yyyy-01-01 for year-of-birth-only age gates). Only stored if the user has none yet}",
"AcceptedTermsAndConditions": true,
"MarketingOptIn": false,
"BrandOwnerOptin": false
}
RESPONSE (200)
{
"success": true,
"sessionId": "{use this for issy-managed loyalty endpoints, see User Session in Get Started}",
"user": {
"id": "{user identifier, use this value as UserId}",
"userName": "{user email}",
"firstName": "{user first name}",
"lastName": "{user last name}",
"email": "{user email}",
"phone": "{user phone}",
"city": "{user city}",
"nickname": "{user nickname}",
"countryCode": "{user country code}",
"termsAcceptance": {
"specificTermsAndConditions": true,
"termsAndConditions": { "required": true, "accepted": false },
"brandOwnerOptin": { "applicable": true, "required": true, "accepted": null },
"marketingOptin": { "accepted": null }
}
}
}

termsAcceptance (same shape as Get Terms Acceptance status below) is only present when PromotionId was sent in the request and matches an existing promotion.

The optional AcceptedTermsAndConditions / MarketingOptIn / BrandOwnerOptin fields (only used together with PromotionId) let you send the consent captured on the login screen: they're stored but never block the login — only the fields that apply to the tenant's mode are kept, and a false/missing AcceptedTermsAndConditions never revokes an earlier acceptance. user.termsAcceptance reflects the status after saving them. See Showing the consent UI.

Error responses:

HTTPerrorCodeCause
400EMAIL_PASSWORD_REQUIREDMissing Password, or neither/both of Email/PhoneNumber were sent.
401INVALID_LOGIN_ATTEMPTNo account for that email/phone, or wrong password.
400INVALID_DATE_OF_BIRTHDateOfBirth was sent but isn't a real past date in yyyy-MM-dd format (or is more than 120 years ago), and it would be stored: the account is new, or it has no birth date yet. Ignored for users who already have one.
406BIRTHDATE_REQUIREDOnly if PromotionId was sent and the promotion has the Age Gate enabled: the user has no birth date stored and DateOfBirth wasn't sent.
406AGE_REQUIREMENT_NOT_METOnly if PromotionId was sent and the promotion has the Age Gate enabled: the user is younger than minimumAge.
401INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500INTERNAL_ERRORUnexpected server error.

Login with verification code​

Passwordless alternative to Login with email and password — for an existing account only (unlike registration, this never creates a new one).

Step 1 — request a code

POST /auth/login/request-code
REQUEST
{
"Email": "{user email - send this OR PhoneNumber}",
"PhoneNumber": "{user phone number - alternative to Email}",
"PromotionId": "{Optional - if the promotion has a custom SMS message configured, it's used instead of the generic one. No effect on email codes.}"
}

Same rate limit as Register with verification code: max 3 requests / 10 minutes, 5 / hour per identifier.

RESPONSE (200)
{
"success": true,
"tokenId": "{opaque id, pass it back to /auth/login/confirm}",
"channel": "{Email or Phone - where the code was sent}",
"expiresInSeconds": 600
}

Error responses:

HTTPerrorCodeCause
400EMAIL_OR_PHONE_REQUIREDNeither or both of Email/PhoneNumber were sent.
404USER_NOT_FOUNDNo account exists for that email/phone.
429TOO_MANY_REQUESTSRate limit exceeded.
401INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500INTERNAL_ERRORUnexpected server error.

Step 2 — confirm the code

POST /auth/login/confirm
REQUEST
{
"TokenId": "{tokenId from the request-code response}",
"Code": "{6-digit code the user received}",
"PromotionId": "{Optional - Promotion id. When sent, the response includes a termsAcceptance block for that promotion and the promotion's Age Gate is enforced}",
"DateOfBirth": "{Optional - user date of birth in format yyyy-mm-dd (yyyy-01-01 for year-of-birth-only age gates). Only stored if the user has none yet}",
"AcceptedTermsAndConditions": true,
"MarketingOptIn": false,
"BrandOwnerOptin": false
}

If the Age Gate rejects the login, the code has already been used — the user must request a new one. Send DateOfBirth on the first attempt to avoid this.

RESPONSE (200)

Same shape as Login with email and password above (sessionId + user, including user.termsAcceptance when PromotionId was sent). The consent fields behave the same way: stored, never blocking.

Error responses:

HTTPerrorCodeCause
404CODE_NOT_FOUNDTokenId doesn't match a pending code.
401CODE_INVALIDCode doesn't match.
401CODE_EXPIREDThe code expired (10 minutes after request-code).
401CODE_ALREADY_USEDThe code was already used to complete a login.
400INVALID_DATE_OF_BIRTHDateOfBirth was sent but isn't a real past date in yyyy-MM-dd format (or is more than 120 years ago), and it would be stored: the account is new, or it has no birth date yet. Ignored for users who already have one.
406BIRTHDATE_REQUIREDOnly if PromotionId was sent and the promotion has the Age Gate enabled: the user has no birth date stored and DateOfBirth wasn't sent.
406AGE_REQUIREMENT_NOT_METOnly if PromotionId was sent and the promotion has the Age Gate enabled: the user is younger than minimumAge.
401INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500INTERNAL_ERRORUnexpected server error.

Login with Google​

The frontend uses Google Sign-In to obtain an id_token, then sends it here to validate it and find or create the matching account.

POST /auth/google
REQUEST
{
"IdToken": "{Google Sign-In id_token}",
"PromotionId": "{Optional - Promotion id. When sent, the response includes a termsAcceptance block for that promotion and the promotion's Age Gate is enforced}",
"DateOfBirth": "{Optional - user date of birth in format yyyy-mm-dd (yyyy-01-01 for year-of-birth-only age gates). Only stored if the user has none yet}",
"AcceptedTermsAndConditions": true,
"MarketingOptIn": false,
"BrandOwnerOptin": false
}
RESPONSE (200)
{
"success": true,
"sessionId": "{use this for issy-managed loyalty endpoints, see User Session in Get Started}",
"user": {
"id": "{user identifier, use this value as UserId}",
"userName": "{user email}",
"firstName": "{user first name}",
"lastName": "{user last name}",
"email": "{user email}",
"phone": "{user phone}",
"city": "{user city}",
"nickname": "{user nickname}",
"countryCode": "{user country code}",
"termsAcceptance": "{Only when PromotionId was sent - same shape as in Login with email and password}"
}
}

If no account exists for the Google account's email, a new one is created automatically (AccountOrigin = Google). It accepts an optional PromotionId to enforce the Age Gate and to return user.termsAcceptance. It never blocks on Terms & Conditions — even when it creates a new account — but it stores the optional consent fields if you send them (same rules as Login with email and password). If the age gate rejects the request, no account and no session are created; ask the user for their date of birth and repeat the call with the same token plus DateOfBirth.

Error responses:

HTTPerrorCodeCause
400ID_TOKEN_REQUIREDMissing IdToken.
401INVALID_GOOGLE_TOKENThe id_token is invalid or expired.
400GOOGLE_EMAIL_MISSINGThe Google account didn't expose an email.
400INVALID_DATE_OF_BIRTHDateOfBirth was sent but isn't a real past date in yyyy-MM-dd format (or is more than 120 years ago), and it would be stored: the account is new, or it has no birth date yet. Ignored for users who already have one.
406BIRTHDATE_REQUIREDOnly if PromotionId was sent and the promotion has the Age Gate enabled: the user has no birth date stored and DateOfBirth wasn't sent.
406AGE_REQUIREMENT_NOT_METOnly if PromotionId was sent and the promotion has the Age Gate enabled: the user is younger than minimumAge.
400USER_CREATION_FAILEDAccount creation failed — message has the details.
401INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500GOOGLE_SIGNIN_NOT_CONFIGUREDServer-side Google Client Id isn't configured.
500INTERNAL_ERRORUnexpected server error.

Login with Facebook​

The frontend uses Facebook Login to obtain an access_token, then sends it here. The token is validated against the Facebook Graph API (/me?fields=id,email,name) to find or create the matching account.

POST /auth/facebook
REQUEST
{
"AccessToken": "{Facebook access_token}",
"PromotionId": "{Optional - Promotion id. When sent, the response includes a termsAcceptance block for that promotion and the promotion's Age Gate is enforced}",
"DateOfBirth": "{Optional - user date of birth in format yyyy-mm-dd (yyyy-01-01 for year-of-birth-only age gates). Only stored if the user has none yet}",
"AcceptedTermsAndConditions": true,
"MarketingOptIn": false,
"BrandOwnerOptin": false
}
RESPONSE (200)
{
"success": true,
"sessionId": "{use this for issy-managed loyalty endpoints, see User Session in Get Started}",
"user": {
"id": "{user identifier, use this value as UserId}",
"userName": "{user email}",
"firstName": "{user first name}",
"lastName": "{user last name}",
"email": "{user email}",
"phone": "{user phone}",
"city": "{user city}",
"nickname": "{user nickname}",
"countryCode": "{user country code}",
"termsAcceptance": "{Only when PromotionId was sent - same shape as in Login with email and password}"
}
}

If no account exists for that email, a new one is created automatically (AccountOrigin = Facebook). It accepts an optional PromotionId to enforce the Age Gate and to return user.termsAcceptance. It never blocks on Terms & Conditions — even when it creates a new account — but it stores the optional consent fields if you send them (same rules as Login with email and password). If the age gate rejects the request, no account and no session are created; ask the user for their date of birth and repeat the call with the same token plus DateOfBirth.

Error responses:

HTTPerrorCodeCause
400ACCESS_TOKEN_REQUIREDMissing AccessToken.
401INVALID_FACEBOOK_TOKENThe access_token is invalid or expired.
400FACEBOOK_EMAIL_MISSINGThe user didn't grant the email permission on Facebook (required, since the account is matched by email).
400INVALID_DATE_OF_BIRTHDateOfBirth was sent but isn't a real past date in yyyy-MM-dd format (or is more than 120 years ago), and it would be stored: the account is new, or it has no birth date yet. Ignored for users who already have one.
406BIRTHDATE_REQUIREDOnly if PromotionId was sent and the promotion has the Age Gate enabled: the user has no birth date stored and DateOfBirth wasn't sent.
406AGE_REQUIREMENT_NOT_METOnly if PromotionId was sent and the promotion has the Age Gate enabled: the user is younger than minimumAge.
400USER_CREATION_FAILEDAccount creation failed — message has the details.
401INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500INTERNAL_ERRORUnexpected server error.

User Participations Profile​

Get information of user participations, credits, ranking position, Terms & Conditions status and SMS notification preferences — scoped to one promotion.

GET /consumerprofile/{promotion id}/{userid}
RESPONSE (200)
{
"userId": "{User identifier}",
"promotionId": "{Promotion id}",
"tenantId": "{TenantId}",
"name": "{Name provided by the user}",
"nickname": "{User nickname}",
"city": "{Name of the city provided by the user}",
"email": "{User Email}",
"phoneNumber": "{User PhoneNumber}",
"creditsEarned": "{Total credits earned in this promotion}",
"creditsSended": "{Total credits sended throw transfers in this promotion}",
"creditsReceived": "{Total credits received throw transfers in this promotion}",
"creditsRedeemed": "{Total credits redeemed for rewards in this promotion}",
"creditsAvailable": "{Total credits available in this promotion}",
"registrationCredits": "{Welcome credits granted at registration for this loyalty promotion, if any. Included in creditsAvailable but NOT in creditsEarned — clients computing a spendable balance from the individual credit fields must add this one in too}",
"rankingPosition": "{If the promotion manages participation ranking, this field shows the user's current position}",
"termsAcceptance": {
"specificTermsAndConditions": true,
"termsAndConditions": { "required": true, "accepted": false },
"brandOwnerOptin": { "applicable": true, "required": true, "accepted": null },
"marketingOptin": { "accepted": null }
},
"notificationPreferences": {
"receiptApproved": true,
"photoApproved": true,
"receiptInvalidated": true,
"photoInvalidated": true,
"topRankingEntered": true
},
"availableNotifications": ["ReceiptApproved", "PhotoApproved", "TopRankingEntered"],
"participations": [
{
"promotionLocationUserId": "{Promotion location user participation Id}",
"dateCreated": "{Participation creation date}",
"status": "{Redemption status of this participation record: 0 => Redeemed, 1 => Pending, 2 => Invalidated}",
"credits": "{Credits earned for this participation}",
"location": "{Location of the participation proof of purchase}",
"hasReceiptImage": "{true if this participation has a stored proof-of-purchase image — from either beginparticipationwithpop/saveparticipation (an invoice) or createparticipationwithphoto}",
"hasPopCode": "{true if this participation was created via createparticipationwithcode (a POP code, not an image)}",
"hasPhotoImage": "{true only when hasReceiptImage's image came from createparticipationwithphoto specifically, not the invoice flow — added 2026-08}",
"imageUrl": "{Blob filename of the stored proof-of-purchase image when hasReceiptImage is true, null otherwise. Not a full URL — resolve it against the public image CDN the same way as other issy image filenames (e.g. a promotion's mainImageUrl): {cdnBase}/{account}/API/GetImage/{imageUrl}}"
}
],
"transfers": [
{
"dateCreated": "{Transfer creation date}",
"userId": "{User identifier}",
"transferType": "{1 => Incoming, 2 => Outgoing}",
"credits": "{transfer credits}"
}
],
"rewardRedemptions": [
{
"dateCreated": "{Redemption creation date}",
"promotionId": "{Reward Id}",
"promotionName": "{Reward Name}",
"redemptionCode": "{QR url or alphanumeric code}",
"redemptionUrl": "{Reference Url to go to redeem the reward code}",
"bannerMessage": "{Per-code banner message configured for this code (Multicode) or reward, when set - null otherwise}",
"credits": "{Used credits}",
"status": "{Redemption status of reward, possible values => PendingReview, Approved, Invalid, Coupon Expired}",
"location": "{Location where the QR code was redeemed}"
}
]
}

termsAcceptance has the same shape used by Get Terms Acceptance status. If the promotion isn't issy-managed, it always reports termsAndConditions: {required: false, accepted: true}.

notificationPreferences and availableNotifications drive the profile's notifications screen — see Notification Preferences. notificationPreferences is what the user chose (every type defaults to true); availableNotifications lists the types this loyalty program actually sends, so show a toggle only for those.

The status field inside participations[] is a RedemptionStatus (0/1/2 as shown above) — a different enum from the PromotionUserStatus used elsewhere in this API (which uses 1/2/3 for Pending/Approved/Invalid). Don't confuse the two when reading this specific array.

hasReceiptImage and hasPopCode are mutually exclusive in practice — a participation comes from either an image (invoice or photo) or the POP-code flow, never both. Use them to tell the two apart (e.g. to decide whether to offer a "view receipt" affordance) instead of inferring it from other fields.

To tell which kind of image it is, check hasPhotoImage only when hasReceiptImage is true: hasPhotoImage: true means createparticipationwithphoto, false means the invoice flow (beginparticipationwithpop/saveparticipation). hasPhotoImage is computed from a field added to ProofOfPurchase in 2026-08 — participations created before that (by either flow) report hasPhotoImage: false, so treat that as "invoice or unknown" rather than a confirmed invoice for old data.

Error responses:

HTTPerrorCodeCause
404USER_NOT_FOUNDuserid doesn't match a registered user.
404PROMOTION_NOT_FOUNDpromotion id doesn't exist.
404PROFILE_NOT_FOUNDThe user has no participation profile for this promotion yet (hasn't registered/participated).
401 / 406INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500INTERNAL_ERRORUnexpected server error.

All User Participations​

Get the full list of a user's participations across all promotions of the tenant (not scoped to a single promotion, unlike User Participations Profile above). Reward redemptions and Loyalty Program participations are excluded from this list.

GET /consumerparticipations/{userid}
RESPONSE (200)
[
{
"id": "{Promotion location user participation Id}",
"promotionLocationId": "{Promotion location id}",
"promotionLocation": {
"promotion": {
"id": "{Promotion id}",
"name": "{Promotion name}",
"code": "{Promotion unique code}"
}
},
"status": "{Participation status 0 => ActivationStarted, 1 => PendingReview, 2 => Approved, 3 => Invalid}",
"statusDescription": "{Localized description of status}",
"dateCreated": "{Participation creation date}",
"dateModified": "{Participation last update date}",
"creditsAssigned": "{Credits assigned by this participation}",
"approvedCreditsAssigned": "{Credits confirmed once the participation is approved}",
"isWinner": "{Whether this participation won a prize}",
"redemptionStatus": "{0 => Redeemed, 1 => Pending, 2 => Invalidated}"
}
]

status here is PromotionUserStatus (0/1/2/3) — note this is a different enum and different field from the status shown under participations[] in User Participations Profile above, which is RedemptionStatus. Same field name, different meaning depending on the endpoint.

The response returns the raw participation records — more fields than shown above may be present; the fields listed here are the ones most relevant for consumption. Results are ordered by DateModified descending. An empty array ([]) means the user exists but has no participations, not an error.

Error responses:

HTTPerrorCodeCause
404USER_NOT_FOUNDuserid doesn't match a registered user.
401 / 406INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500INTERNAL_ERRORUnexpected server error.

Terms & Conditions Acceptance​

Query or save a user's Terms & Conditions / Privacy Policy consent for a promotion, independently of registration or participation. Useful to check status upfront and show your own consent UI, or to record consent (or revoke it, by sending false) at any other point in your flow. See Terms & Conditions Acceptance in Get Started for the full concept.

Get Terms display​

Returns only what's needed to render the consent UI for a promotion / loyalty program — no user involved, so use it on register screens where the user doesn't exist yet. See Showing the consent UI in Get Started for how to use each field.

GET /termsacceptance/{promotion id}?lang={optional: en | es | fr | pt}
RESPONSE (200) - specific mode
{
"display": {
"requireIssyTermsAcceptance": true,
"specificTermsAndConditions": true,
"termsAndConditionsText": "<p>This is the space where the Terms and Conditions of the <b><u>Hennessy Bahamas Loyalty Program</u></b> will be displayed.</p>",
"policyPrivacyUrl": "https://www.example.com/privacy",
"showTitle": true,
"title1": "To continue please read the",
"title2": "DATA PROTECTION INFORMATION",
"title2Url": "https://www.example.com/data-protection",
"brandOwner": {
"id": "{brand owner id}",
"name": "Hennessy",
"siteUrl": "https://www.hennessy.com"
},
"issyTermsUrl": null,
"issyPrivacyPolicyUrl": null
}
}
RESPONSE (200) - simple mode
{
"display": {
"requireIssyTermsAcceptance": true,
"specificTermsAndConditions": false,
"termsAndConditionsText": "<p>...</p>",
"policyPrivacyUrl": null,
"showTitle": false,
"title1": null,
"title2": null,
"title2Url": null,
"brandOwner": null,
"issyTermsUrl": "https://{issy web}/{tenant}/Terms/en",
"issyPrivacyPolicyUrl": "https://{issy web}/{tenant}/PrivacyPolicy/en"
}
}

Field reference (display):

  • requireIssyTermsAcceptance: false when the promotion is client-managed — issy doesn't track consent for it, so don't show issy's consent UI. Same value as the promotion's APIRequireIssyTermsAcceptance.
  • specificTermsAndConditions: the tenant's mode.
  • termsAndConditionsText: the promotion's Terms & Conditions as an HTML string (render it sanitized, typically in a modal). null when the promotion has none.
  • Specific mode only (null/false in simple mode):
    • policyPrivacyUrl: the promotion's privacy policy URL. null if not configured.
    • showTitle, title1, title2, title2Url: optional heading shown above the checkboxes. The titles are null when showTitle is false.
    • brandOwner: the tenant's brand owner, for the optional marketing opt-in label (name, link to siteUrl). null if the tenant has none — then there's no brand opt-in.
  • Simple mode only (null in specific mode):
    • issyTermsUrl, issyPrivacyPolicyUrl: public pages with issy's own Terms & Conditions and Privacy Policy, in the requested language.

lang only affects issyTermsUrl/issyPrivacyPolicyUrl. If omitted, the language of the promotion's country is used; anything other than en, es, fr or pt falls back to en.

Error responses:

HTTPerrorCodeCause
404PROMOTION_NOT_FOUNDpromotion id doesn't exist or isn't currently available.
401 / 406INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500INTERNAL_ERRORUnexpected server error.

Get Terms Acceptance status​

Returns the user's consent status plus the same display block as Get Terms display.

GET /termsacceptance/{promotion id}/{userid}?lang={optional: en | es | fr | pt}
RESPONSE (200)
{
"specificTermsAndConditions": true,
"termsAndConditions": {
"required": true,
"accepted": false
},
"brandOwnerOptin": {
"applicable": true,
"required": true,
"accepted": null
},
"marketingOptin": {
"accepted": null
},
"display": {
"requireIssyTermsAcceptance": true,
"specificTermsAndConditions": true,
"termsAndConditionsText": "<p>This is the space where the Terms and Conditions of the <b><u>Hennessy Bahamas Loyalty Program</u></b> will be displayed.</p>",
"policyPrivacyUrl": "https://www.example.com/privacy",
"showTitle": true,
"title1": "To continue please read the",
"title2": "DATA PROTECTION INFORMATION",
"title2Url": "https://www.example.com/data-protection",
"brandOwner": {
"id": "{brand owner id}",
"name": "Hennessy",
"siteUrl": "https://www.hennessy.com"
},
"issyTermsUrl": null,
"issyPrivacyPolicyUrl": null
}
}

Field reference:

  • specificTermsAndConditions: the tenant's mode (see Terms & Conditions Acceptance in Get Started). Always true/false.
  • termsAndConditions.required: false only when the promotion isn't issy-managed — in that case accepted is always reported true and the rest of the object is inert.
  • termsAndConditions.accepted: whether this user has already accepted. In simple mode this is always false (no persisted state — see Get Started).
  • brandOwnerOptin.applicable: true only in specific mode with a tenant that has a brand owner.
  • brandOwnerOptin.required: true only when applicable and the user has never answered the brand-owner opt-in. It means "an answer is still needed" (true or false), not "must accept" — marketing consent is optional.
  • brandOwnerOptin.accepted: true = opted in, false = declined, null = never answered (or not applicable).
  • marketingOptin.accepted: only meaningful in simple mode — the user's current MarketingOptIn value on their account (null if never set); always null in specific mode.
  • display: see Get Terms display.

Error responses:

HTTPerrorCodeCause
404PROMOTION_NOT_FOUNDpromotion id doesn't exist.
404USER_NOT_FOUNDuserid doesn't match a registered user.
401 / 406INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500INTERNAL_ERRORUnexpected server error.

Save Terms Acceptance​

Unlike the gate on registration/participation endpoints, this endpoint never returns 412 — it always saves whatever you send (including false, to revoke).

POST /termsacceptance
REQUEST
{
"UserId": "{user identifier}",
"PromotionId": "{Promotion id}",
"AcceptedTermsAndConditions": true,
"MarketingOptIn": true,
"BrandOwnerOptin": true
}

Send only the field(s) that apply to the tenant's mode — MarketingOptIn in simple mode, BrandOwnerOptin in specific mode (never both). If the promotion isn't issy-managed, the call succeeds without persisting anything. In specific mode, send BrandOwnerOptin as true or false (the checkbox value) so the user's answer is recorded; the two opt-ins are stored independently. If the tenant has no brand owner, BrandOwnerOptin is ignored.

AcceptedTermsAndConditions is optional: if you leave it out, the user's stored acceptance doesn't change. Send false only when you really want to revoke it. Revoking blocks the user, because accepted Terms & Conditions are required to keep using the program.

To change only the brand opt-in, send just BrandOwnerOptin. This works for a "stop marketing from {brand}" button (false) or to opt back in (true):

REQUEST - brand opt-in only
{
"UserId": "{user identifier}",
"PromotionId": "{Promotion id}",
"BrandOwnerOptin": false
}

The current value is in termsAcceptance.brandOwnerOptin.accepted of User Participations Profile.

RESPONSE (200)
{
"success": true
}

Error responses:

HTTPerrorCodeCause
400USER_AND_PROMOTION_REQUIREDUserId/PromotionId missing.
404PROMOTION_NOT_FOUNDPromotionId doesn't exist.
404USER_NOT_FOUNDUserId doesn't match a registered user.
400TERMS_FIELD_NOT_APPLICABLESent BrandOwnerOptin in simple mode, or MarketingOptIn in specific mode.
401 / 406INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500INTERNAL_ERRORUnexpected server error.

Notification Preferences​

When a loyalty program is issy-managed, issy sends SMS notifications to its participants. Each type can be turned on or off by the program in the backoffice, and by each user for themselves. A user receives a notification only when both have it on, and only if their account has a phone number.

These are transactional notifications about the user's own activity in the program. They are separate from the brand's marketing consent (BrandOwnerOptin / MarketingOptIn), which is managed with Save Terms Acceptance.

TypeSent when
ReceiptApprovedA participation made with a receipt / invoice is approved.
PhotoApprovedA participation made with a photo is approved.
ReceiptInvalidatedA participation made with a receipt / invoice is invalidated.
PhotoInvalidatedA participation made with a photo is invalidated.
TopRankingEnteredThe user enters the program's top ranking for the first time. Sent once per user and program, never again.

Participations made with a POP code don't send notifications: they are validated at the moment the code is used.

Preferences are per user and per loyalty program. To read them, use User Participations Profile: notificationPreferences has the user's current values, and availableNotifications lists the types the program sends.

Update notification preferences​

Partial update: only the preferences present in the request change, and the ones you don't send keep their value. Send all of them from a settings screen, or only the toggle the user just changed.

POST /updatenotificationpreferences
REQUEST
{
"UserId": "{user identifier}",
"PromotionId": "{Loyalty program id}",
"ReceiptApproved": true,
"PhotoApproved": true,
"ReceiptInvalidated": false,
"PhotoInvalidated": false,
"TopRankingEntered": true
}

UserId accepts the user id, email or phone, like the other user endpoints. Every preference is optional (true / false), but send at least one.

RESPONSE (200)
{
"preferences": {
"receiptApproved": true,
"photoApproved": true,
"receiptInvalidated": false,
"photoInvalidated": false,
"topRankingEntered": true
},
"availableNotifications": ["ReceiptApproved", "PhotoApproved", "TopRankingEntered"]
}

The response always has all the preferences with their current values, plus availableNotifications (same as in the profile), so you can refresh the screen without another call.

You can save a preference for a type that isn't in availableNotifications. It's stored, but has no effect until the program turns that type on.

Error responses:

HTTPerrorCodeCause
400USER_AND_PROMOTION_REQUIREDUserId/PromotionId missing.
400NO_PREFERENCES_SENTThe request has no preference to update.
404PROMOTION_NOT_FOUNDPromotionId doesn't exist or isn't currently available.
406PROMOTION_INVALID_FOR_ACTIONPromotionId is not a loyalty program.
404USER_NOT_FOUNDUserId doesn't match a registered user.
404PROFILE_NOT_FOUNDThe user has no participation profile for this program yet (hasn't registered/participated).
401 / 406INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500INTERNAL_ERRORUnexpected server error.

Promotion Ranking​

Shows the list of the first N places in the promotion participation ranking and the current position of the user that is sent as a parameter.

GET /ranking/{promotion id}/{userid}/{positions}
RESPONSE (200)
{
"userRankingPosition": "{Current position of the user passed as parameter}",
"credits": "{Credits accumulated by the user}",
"ranking": [
{
"nickname": "{User nickname}",
"credits": "{Credits accumulated by the user}",
"userRankingPosition": "{Position}",
"city": "{User city}"
}
]
}

positions controls how many entries ranking returns (defaults to 10 if not a valid number).

Error responses:

HTTPerrorCodeCause
404PROMOTION_NOT_FOUNDpromotion id doesn't exist.
404USER_NOT_FOUNDuserid doesn't match a registered user.
404PROFILE_NOT_FOUNDThe user has no participation profile for this promotion yet.
401 / 406INVALID_HEADER_CONTENTMissing/invalid issy-token or issy-tenant-id.
500INTERNAL_ERRORUnexpected server error.