Authentication
API v2 uses JWT tokens for authentication. Users authenticate via magic link (email code) or social login, and receive a JWT token that must be included in all subsequent requests.
Login with Magic Link
Two-step flow: request a code via email, then validate it to receive a JWT.
Step 1: Request Code
POST /api/v2/login/token/init
Sends a temporary login code to the user's email. Creates the user account if it does not exist for the given tenant.
Request:
curl -X POST https://app.publica.la/api/v2/login/token/init \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"email": "user@example.com"
}'
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Yes | User email address |
Optional Headers:
| Header | Description |
|---|---|
X-CustomFenice-Tenant-Id | Tenant ID (defaults to Publica.la) |
X-Fenice-Lang | Email language: en, es, pt, it (defaults to en) |
Response: 201 Created with empty body.
Step 2: Validate Code
POST /api/v2/login/token/validate
Validates the code received via email and returns a JWT token.
Request:
curl -X POST https://app.publica.la/api/v2/login/token/validate \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"email": "user@example.com",
"code": "12345678"
}'
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Same email used in init |
code | string | Yes | Code from email (8 characters) |
Response: 200 OK
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "k4q6r8s0t2u4v6w8x0y2z4a6b8c0d2e4f6g8h0i2j4k6l8m0n2o4p6q8r0s2t4u6"
}
| Field | Type | Description |
|---|---|---|
access_token | string | JWT to send in Authorization: Bearer ... headers |
token_type | string | Always bearer |
expires_in | integer | Access token TTL in seconds |
refresh_token | string | 64-character opaque token used to rotate the access token. See Refresh Token below |
The code length is configured per environment. Production uses 8 characters.
Social Login
Single-step authentication via OAuth providers.
POST /api/v2/login/social/{provider}
Supported providers: google, facebook, apple.
Request:
curl -X POST https://app.publica.la/api/v2/login/social/google \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"access_token": "ya29.a0AfH6SM..."
}'
| Parameter | Type | Required | Description |
|---|---|---|---|
access_token | string | Yes | OAuth token from the provider |
For Apple, the client receives both id_token and access_token. Send the id_token as the access_token parameter.
Response: 200 OK - Same JWT response as magic link validation, including refresh_token.
Error Responses:
| Code | Meaning |
|---|---|
| 400 | Invalid provider or bad request |
| 401 | User declined authorization on provider |
| 422 | Email not available from social provider |
Refresh Token
POST /api/v2/auth/refresh
Rotates an expired or about-to-expire access token without forcing the user to re-authenticate. The previous refresh token is invalidated and a brand new JWT plus refresh token are returned.
Authentication: None. The refresh_token itself is the credential. Do not send Authorization: Bearer ....
Rate limit: 10 requests per minute per IP.
Request:
curl -X POST https://app.publica.la/api/v2/auth/refresh \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-CustomFenice-Tenant-Id: 1" \
-d '{
"refresh_token": "k4q6r8s0t2u4v6w8x0y2z4a6b8c0d2e4f6g8h0i2j4k6l8m0n2o4p6q8r0s2t4u6"
}'
| Parameter | Type | Required | Description |
|---|---|---|---|
refresh_token | string | Yes | The opaque token returned by login (or by a previous refresh) |
Optional Headers:
| Header | Description |
|---|---|
X-CustomFenice-Tenant-Id | Custom app tenant ID (defaults to Publica.la, ID 1) |
Response: 200 OK
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "n8o0p2q4r6s8t0u2v4w6x8y0z2a4b6c8d0e2f4g6h8i0j2k4l6m8n0o2p4q6r8s0"
}
The new refresh_token replaces the old one. Clients must store and use the new value going forward; the old refresh token is no longer valid after a successful rotation. Concurrent refresh requests with the same token are safe: only one rotation succeeds.
Error Responses
| Code | Cause |
|---|---|
| 401 | Refresh token invalid, expired, or already rotated |
| 422 | refresh_token field missing |
| 429 | Rate limit exceeded (10 requests per minute per IP) |
Session Validation
GET /api/v2/auth/session/validate
Confirms that the current JWT session is still valid and within concurrency limits. This endpoint does not perform business logic; it relies entirely on the middleware stack.
Request:
curl https://app.publica.la/api/v2/auth/session/validate \
-H "Authorization: Bearer {access_token}" \
-H "Accept: application/json"
Response: 200 OK
{
"CODE": "success",
"message": "Session is valid"
}
| Code | Meaning |
|---|---|
| 200 | Session active and within limits |
| 401 | Invalid or expired session |
| 409 | Session limit reached for this user |
Logout
GET /api/v2/logout
Invalidates the mobile session across all devices using this JWT.
Request:
curl https://app.publica.la/api/v2/logout \
-H "Authorization: Bearer {access_token}" \
-H "Accept: application/json"
Response: 200 OK with empty body.
Deletes all mobile sessions matching the JWT's jti claim.
Using the Token
Include the JWT in all authenticated requests:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
The token contains:
email- User emailjti- Unique token ID (used for session tracking)exp- Expiration timestamp
See Also
- API v2 Overview - Headers, tenant modes, error handling
- User Endpoints - User profile and push notifications