REST API Reference¶
Overview¶
The Gavekort API is exposed via Google Cloud Functions HTTP endpoints with Firebase ID token authentication.
All REST endpoints require:
- Authorization: Bearer <firebase-id-token> header
- Content-Type: application/json for POST requests
Base URL¶
https://your-project-region-gcp.cloudfunctions.net/
Authentication¶
Firebase ID tokens are obtained from Firebase Authentication. Pass the token in the Authorization header:
curl -H "Authorization: Bearer $(firebase auth:sign-in-as --email user@example.com)" \
https://your-project-region.cloudfunctions.net/giftcards_issue \
-d '{"productId": "giftcard-100nok", ...}'
Health Check¶
Endpoint: GET /healthCheck
Authentication: None required
Role: Public
Response¶
{
"status": "ok",
"timestamp": "2024-01-15T10:30:00.000Z"
}
Giftcard Management¶
Issue Giftcard¶
Endpoint: POST /giftcards_issue
Authentication: Required (Firebase ID token)
Role: ADMIN only
Issues a new giftcard to a user. Supports optional claim codes for gift distribution.
Request¶
{
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"productId": "giftcard-100nok",
"targetUserId": "user-recipient-id",
"claimCode": "XXXX-XXXX-XXXX-XXXX",
"reason": "Birthday gift"
}
| Field | Type | Required | Description |
|---|---|---|---|
idempotencyKey |
UUID | ✓ | Unique idempotency key for exactly-once semantics |
productId |
string | ✓ | Product ID (e.g., giftcard-100nok, token-10count) |
targetUserId |
string | ✗ | Target user (defaults to requester if omitted) |
claimCode |
string | ✗ | Optional claim code for giftcard distribution |
reason |
string | ✗ | Reason for issuance (logged in audit trail) |
Response¶
{
"success": true,
"data": {
"walletItemId": "item-550e8400-e29b-41d4",
"ledgerTxId": "tx-550e8400-e29b-41d4",
"claimCodeHash": "sha256(code+pepper)"
}
}
Errors¶
| Status | Code | Message |
|---|---|---|
| 400 | INVALID_ARGUMENT |
Missing required field or invalid product |
| 401 | UNAUTHENTICATED |
Invalid or missing Firebase ID token |
| 403 | PERMISSION_DENIED |
User must have ADMIN role |
| 500 | INTERNAL |
Server error |
Example¶
curl -X POST https://your-project.cloudfunctions.net/giftcards_issue \
-H "Authorization: Bearer $ID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"productId": "giftcard-100nok",
"targetUserId": "user-john-doe",
"reason": "Birthday gift"
}'
Wallet Operations¶
Redeem Value¶
Endpoint: POST /wallet_redeem
Authentication: Required (Firebase ID token)
Role: END_USER (own items), STAFF, ADMIN (any items)
Deduct value from a wallet item (giftcard, token, or ticket).
Request¶
{
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"walletItemId": "item-550e8400",
"amount": 50,
"reason": "Greens fee payment"
}
| Field | Type | Required | Description |
|---|---|---|---|
idempotencyKey |
UUID | ✓ | Unique idempotency key |
walletItemId |
string | ✓ | Wallet item ID to redeem from |
amount |
number | ✓ | Amount to redeem (NOK for giftcards, count for tokens) |
reason |
string | ✗ | Redemption reason |
Response¶
{
"success": true,
"data": {
"ledgerTxId": "tx-550e8400-e29b-41d4",
"remainingBalance": 50
}
}
Errors¶
| Status | Code | Message |
|---|---|---|
| 400 | INVALID_ARGUMENT |
Missing required field or invalid amount |
| 401 | UNAUTHENTICATED |
Invalid or missing Firebase ID token |
| 403 | PERMISSION_DENIED |
User cannot redeem other users' items |
| 404 | NOT_FOUND |
Wallet item not found |
| 500 | INTERNAL |
Server error |
Example¶
curl -X POST https://your-project.cloudfunctions.net/wallet_redeem \
-H "Authorization: Bearer $ID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"walletItemId": "item-550e8400",
"amount": 50,
"reason": "Course payment"
}'
Claim Code¶
Endpoint: POST /wallet_claim
Authentication: Required (Firebase ID token)
Role: END_USER
End user claims a giftcard using a code. Creates a new wallet item for the user.
Request¶
{
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"code": "XXXX-XXXX-XXXX-XXXX",
"externalAccountId": "member-12345"
}
| Field | Type | Required | Description |
|---|---|---|---|
idempotencyKey |
UUID | ✓ | Unique idempotency key |
code |
string | ✓ | Claim code (e.g., ABCD-EFGH-IJKL-MNOP) |
externalAccountId |
string | ✗ | Club membership ID (for linking) |
Response¶
{
"success": true,
"data": {
"walletItemId": "item-550e8400-e29b-41d4",
"ledgerTxId": "tx-550e8400-e29b-41d4"
}
}
Errors¶
| Status | Code | Message |
|---|---|---|
| 400 | INVALID_ARGUMENT |
Missing required field |
| 401 | UNAUTHENTICATED |
Invalid or missing Firebase ID token |
| 404 | NOT_FOUND |
Code not found, already claimed, or expired |
| 500 | INTERNAL |
Server error |
Example¶
curl -X POST https://your-project.cloudfunctions.net/wallet_claim \
-H "Authorization: Bearer $ID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"code": "ABCD-EFGH-IJKL-MNOP"
}'
Wallet Projection¶
Get Wallet Projection¶
Endpoint: GET /wallet_projections/:userId
Authentication: Required (Firebase ID token)
Role: END_USER (own wallet), STAFF, ADMIN (any wallet)
Get aggregated wallet balance and recent transactions.
Path Parameters¶
| Parameter | Type | Description |
|---|---|---|
userId |
string | User ID to retrieve wallet for |
Response¶
{
"success": true,
"data": {
"userId": "user-john-doe",
"totalGiftcardBalance": 500,
"totalRangeTokens": 10,
"greenfeeTickets": {
"9_HOLES": 5,
"18_HOLES": 2
},
"activeWalletItems": 8,
"recentTransactions": [
{
"txId": "tx-550e8400",
"type": "REDEEM",
"amount": 50,
"timestamp": "2024-01-15T10:30:00.000Z"
}
],
"lastUpdatedAt": "2024-01-15T10:30:00.000Z",
"ledgerVersion": 42
}
}
Errors¶
| Status | Code | Message |
|---|---|---|
| 401 | UNAUTHENTICATED |
Invalid or missing Firebase ID token |
| 403 | PERMISSION_DENIED |
Cannot access this user's wallet |
| 404 | NOT_FOUND |
Wallet projection not found |
| 500 | INTERNAL |
Server error |
Example¶
curl -X GET https://your-project.cloudfunctions.net/wallet_projections/user-john-doe \
-H "Authorization: Bearer $ID_TOKEN" \
-H "Content-Type: application/json"
Idempotency & Retries¶
All POST endpoints support idempotent retries via the idempotencyKey field.
If a request fails, retry with the same idempotencyKey. The server will:
1. Return the cached result if the operation completed
2. Return a failed-precondition if still processing
3. Retry once more if infrastructure error occurred
Example retry logic:
async function issueGiftcardWithRetry(data, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(
'https://your-project.cloudfunctions.net/giftcards_issue',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${idToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}
);
if (response.ok) return await response.json();
// 409 conflict or 429 rate limit - retry with same idempotencyKey
if (response.status === 409 || response.status === 429) {
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
continue;
}
throw new Error(`HTTP ${response.status}`);
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
}
}
}
Error Handling¶
All endpoints return standard error responses:
{
"code": "INVALID_ARGUMENT|UNAUTHENTICATED|PERMISSION_DENIED|NOT_FOUND|INTERNAL|...",
"message": "Human-readable error description"
}
Error Codes¶
INVALID_ARGUMENT: Request validation failedUNAUTHENTICATED: Firebase ID token invalid or missingPERMISSION_DENIED: User lacks required roleNOT_FOUND: Resource not foundINTERNAL: Server-side error
OpenAPI/Swagger Specification¶
Full OpenAPI 3.0 specification available in openapi.yaml:
# Validate schema
npx @stoplight/spectacle-cli build -o ./docs/openapi.html openapi.yaml
# Generate SDK (JavaScript example)
npx openapi-generator-cli generate -i openapi.yaml -g javascript -o ./sdk