Profile API
User profile view, update, PDPL data portability export, and account deletion.
Per T-06-07: No endpoint accepts
userIdfrom the request body. All operations are scoped to the authenticated user's JWT.
GET /profile
Return the authenticated user's profile aggregate, including activated SIM count, eSIM-store currency preference, and notification locale.
Requires authentication.
Example Request
GET /profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Response
200 OK
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"phone": "+966501234567",
"email": "ahmed@example.com",
"name": "Ahmed Al-Rashid",
"identityType": "citizen",
"identityTypeLocked": false,
"appRole": "end_user",
"preferredCurrency": "SAR",
"locale": "ar",
"activatedSimCount": 3,
"createdAt": "2026-04-30T12:00:00.000Z"
}For email-only users,
phoneisnull. For phone-only users,null.identityTypeLockedistruewhile an identity check for this customer is waiting, for good once one has completed, and always for a company employee whose type comes from the roster. It turnsfalseagain when a waiting check fails, is rejected, errors or expires.falsemeansidentityTypecan still be changed.preferredCurrencyisUSD,SAR,AED,EUR,GBP,EGP, ornull.nullmeans store browse uses the explicit request currency first, then the USD base default. On store checkout it is the fallback order currency when the request sends none.localeisenorar, ornullwhen unset. Registration fills it from the request'sAccept-Languageheader, and the profile update changes it later. It selects the language of notification inbox copy, notification SMS text, and custom notification emails.nullrenders in English. See notification rendering.
Error Responses
| Code | Status | Condition |
|---|---|---|
AUTH_REQUIRED | 401 | Missing or invalid token |
PUT /profile
Update allowed profile fields. name, email, preferredCurrency, locale, and identityType can be updated here. Phone changes require POST /auth/change-phone with OTP verification.
Requires authentication.
Per T-06-08: Zod validation and whitelist — only
name,preferredCurrency,locale, andidentityTypeare accepted.
The identity type and the other fields are saved in one transaction, so the update fully happens or not at all. A refused type change leaves the rest of the request untouched. It is refused with 409 IDENTITY_TYPE_LOCKED while an identity check for this customer is waiting or once one has completed, and with 409 IDENTITY_TYPE_COMPANY_MANAGED for a company employee, whose type comes from the company's roster. Sending the type already stored is never refused, locked or not, and records nothing. A change is audited as user.identity_type_changed with the customer as actor, the old and new type, and source: "profile_update".
email follows the same rules as PUT /auth/me: a new email rewrites emailHash so it becomes the login email as typed, an email another account owns is refused with 409 AUTH_EMAIL_EXISTS, null removes the email and is refused with 409 AUTH_LAST_IDENTIFIER when the account has no phone, and a company admin's new email fills the company's contact email while that is still empty and the company is not archived. A change to name or email is audited as profile.updated with the names of the changed fields only, never their values. preferredCurrency and locale are not audited, and a save that changes neither name nor email writes no audit row.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Updated name (1–255 characters) |
email | string | null | No | Updated email address. Send null to remove it; refused when the account has no phone |
identityType | citizen | resident | visitor | No | New identity type. Refused once locked |
preferredCurrency | USD | SAR | AED | EUR | GBP | EGP | null | No | Persistent eSIM-store presentment preference. Send null to clear it |
locale | en | ar | null | No | Language for notification inbox copy, notification SMS text, and custom notification emails. Send null to clear it, which renders in English |
Example Request
PUT /profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json{
"name": "Ahmed Mohammed Al-Rashid",
"email": "ahmed.new@example.com",
"preferredCurrency": "SAR",
"locale": "ar"
}Partial update (currency clear only):
{
"preferredCurrency": null
}Response
200 OK
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"phone": "+966501234567",
"email": "ahmed.new@example.com",
"name": "Ahmed Mohammed Al-Rashid",
"identityType": "citizen",
"identityTypeLocked": false,
"appRole": "end_user",
"preferredCurrency": "SAR",
"locale": "ar",
"activatedSimCount": 3,
"createdAt": "2026-04-30T12:00:00.000Z"
}
nullafter a request that removed it.
Error Responses
| Code | Status | Condition |
|---|---|---|
AUTH_REQUIRED | 401 | Missing or invalid token |
VALIDATION_ERROR | 400 | Invalid name, email, currency, locale, or identityType |
IDENTITY_TYPE_LOCKED | 409 | An identity check is waiting or has already completed |
IDENTITY_TYPE_COMPANY_MANAGED | 409 | The account is a company employee |
AUTH_EMAIL_EXISTS | 409 | Another account already uses the email |
AUTH_LAST_IDENTIFIER | 409 | email: null on an account with no phone, which would remove its only sign-in identifier |
GET /profile/export
PDPL (Personal Data Protection Law) data portability export. Returns all user data in a structured format with compliance metadata.
Requires authentication.
Per T-06-07: Export is scoped to the JWT owner only. No cross-user lookup path exists.
The export includes notifications and notificationOptOuts alongside profile, activation, order, payment, and store-order data. Notification history includes archived, expired, and scrubbed rows. It returns stored parameters, not rendered inbox text.
| Export Entry | Fields |
|---|---|
notifications[] | id, category, eventKey, templateKey, params, severity, createdAt, readAt, seenAt, archivedAt, expiresAt, deliveries |
notifications[].deliveries[] | channel, outcome, provider, attempts, sentAt, deliveredAt, failedAt, suppressionReason |
notificationOptOuts[] | channel, createdAt |
Scrubbed notification parameters are {}. The export excludes batch requests, callback receipts, provider message IDs, error text, dedupe keys, and idempotency keys. See notification retention and export.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
format | string | json | Export format (currently only json supported) |
Example Request
GET /profile/export?format=json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Response
200 OK
{
"format": "json",
"exportedAt": "2026-05-05T08:30:00.000Z",
"data": {
"profile": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"phone": "+966501234567",
"email": "ahmed@example.com",
"name": "Ahmed Al-Rashid",
"identityType": "citizen",
"appRole": "end_user",
"preferredCurrency": "SAR",
"locale": "ar",
"createdAt": "2026-04-30T12:00:00.000Z"
},
"activations": [
{
"id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"iccid": "LPA:1$SMDP.DEV.SKYTE.SA$***",
"simType": "esim",
"smdpAddress": "SMDP.DEV.SKYTE.SA",
"matchingId": "SKYTE-DEV-0001",
"activationCodeRaw": "LPA:1$SMDP.DEV.SKYTE.SA$SKYTE-DEV-0001",
"status": "activated",
"createdAt": "2026-05-05T08:30:00.000Z"
}
],
"orders": [],
"payments": [],
"storeOrders": [
{
"orderId": "550e8400-e29b-41d4-a716-44665544e401",
"orderStatus": "completed",
"product": {
"id": "550e8400-e29b-41d4-a716-44665544e101",
"nameEn": "Saudi Arabia 3 GB",
"nameAr": "السعودية 3 جيجابايت"
},
"presentmentCurrency": "SAR",
"presentmentAmount": "18.71",
"fxRateUsed": "3.75",
"retailUsdSnapshot": "4.99",
"smdpAddress": "SMDP.STORE.DEV.SKYTE.SA",
"matchingId": "STORE-8F3A1C2B",
"activationCodeRaw": "1$SMDP.STORE.DEV.SKYTE.SA$STORE-8F3A1C2B",
"iccid": "8900000000000000001",
"issuedAt": "2026-05-05T08:32:00.000Z",
"createdAt": "2026-05-05T08:30:00.000Z"
}
],
"notifications": [],
"notificationOptOuts": []
},
"metadata": {
"pdplCompliant": true,
"requestId": "export-a1b2c3d4-20260505",
"retentionPolicyDays": 365
}
}Error Responses
| Code | Status | Condition |
|---|---|---|
AUTH_REQUIRED | 401 | Missing or invalid token |
DELETE /profile
Request account deletion. This is a soft delete — the user record is marked inactive with a deletionRequestedAt timestamp. Data is retained during the mandatory PDPL retention period, then purged.
Requires authentication. Customers (end_user) only; other roles get 403 AUTH_FORBIDDEN.
The request also stamps the retention lifecycle on the caller's records. Activations and store orders both get deletionRequestedAt, and store orders additionally get retainUntil set to the request time plus the 365-day retention policy. Once that date passes, POST /compliance/retention/sweep scrubs the store order's eSIM payload — smdpAddress, matchingId, activationCodeRaw and iccid are nulled and deletedAt is stamped. The order row itself is kept because the purchase and its payment are a financial record.
Existing notifications also receive deletionRequestedAt and the 365-day retainUntil deadline, and notifications recorded for the account later receive the same two stamps from the request's audit event. The flag, the stamps and the audit event are written in one transaction. After that deadline, the retention sweep clears their params and stamps deletedAt. Notification rows, dedupe keys, and delivery history remain. Notification expiry or archiving alone does not trigger scrubbing. Batch requests, callback receipts, and channel opt-outs are not cleared by this sweep.
Per T-06-09: Auditable with
PROFILE_DELETION_REQUESTEDevent. Per T-06-07: Uses JWT contextuserIdonly.
Request Body (Optional)
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | No | Optional reason for deletion |
Example Request
DELETE /profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json{
"reason": "No longer need the service"
}Without body:
DELETE /profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Response
200 OK
{
"deleted": true,
"purgedAt": null,
"message": "Account deletion requested. Your data will be retained during the mandatory retention period, then permanently deleted."
}Arabic response (Accept-Language: ar):
{
"deleted": true,
"purgedAt": null,
"message": "تم طلب حذف الحساب. ستُحتفظ بياناتك خلال فترة الاحتفاظ الإلزامية ثم تُحذف نهائيًا."
}
purgedAtisnullwhile data retention is pending. Once fully purged,purgedAtcontains the timestamp.
Error Responses
| Code | Status | Condition |
|---|---|---|
AUTH_REQUIRED | 401 | Missing or invalid token |