POS Service -- Getting Started
This guide gets you connected to the POS service and making your first API calls in 15 minutes.
What is POS?
The POS service is the source of truth for all point-of-sale transactional operations. It manages catalog (products, variants, modifiers, pricing, tax rules), sales (orders, payments), inventory (stock, locations, purchase orders), customers (profiles, loyalty, points), staff (shifts, drawers, commissions), promotions (coupons, discounts), and payment gateway integration (Midtrans).
POS does not handle authentication (the Auth service does) or identity/permissions (the IAM service does).
Connection Setup
Import the generated stubs
Add the POS module as a dependency:
go get github.com/TopengDev/aenoxa_pos@latest
Create a gRPC connection
package main
import (
"log"
posv1 "github.com/TopengDev/aenoxa_pos/gen/pos/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.NewClient("localhost:50053",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
defer conn.Close()
catalogClient := posv1.NewCatalogServiceClient(conn)
salesClient := posv1.NewSalesServiceClient(conn)
inventoryClient := posv1.NewInventoryServiceClient(conn)
customerClient := posv1.NewCustomerServiceClient(conn)
staffClient := posv1.NewStaffServiceClient(conn)
promotionClient := posv1.NewPromotionServiceClient(conn)
paymentClient := posv1.NewPaymentTransactionServiceClient(conn)
// Use the clients...
_ = catalogClient
_ = salesClient
_ = inventoryClient
_ = customerClient
_ = staffClient
_ = promotionClient
_ = paymentClient
}
Authentication
POS supports three authentication modes:
Development mode (default)
When POS_AUTH_ENABLED=false, authentication is bypassed. Use dev headers to simulate tenant context:
import "google.golang.org/grpc/metadata"
ctx := metadata.AppendToOutgoingContext(ctx,
"x-dev-tenant-id", "00000000-0000-0000-0000-000000000001",
"x-dev-user-id", "00000000-0000-0000-0000-000000000001",
"x-dev-membership-id", "00000000-0000-0000-0000-000000000001",
)
JWT authentication
When POS_AUTH_ENABLED=true, attach a tenant-scoped JWT from the Auth service:
ctx := metadata.AppendToOutgoingContext(ctx,
"authorization", "Bearer "+accessToken,
)
The JWT must be an Ed25519-signed tenant-scoped token with tenant_id, membership_id, and scope claims. POS validates the signature by fetching JWKS from the Auth service.
API key authentication
For service-to-service calls:
ctx := metadata.AppendToOutgoingContext(ctx,
"x-api-key", "your-api-key",
)
Quick Start: Create a Product and Place an Order
1. Health check
grpcurl -plaintext localhost:50053 pos.v1.HealthService/Check
2. Create a category
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"name": "Beverages", "sort_order": 1}' \
localhost:50053 pos.v1.CatalogService/CreateCategory
3. Create a product
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"name": "Iced Latte", "sku": "ICE-LATTE-001", "category_id": "<category_id>"}' \
localhost:50053 pos.v1.CatalogService/CreateProduct
4. Create a variant and set a price
# Create variant
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"product_id": "<product_id>", "name": "Regular", "sku": "ICE-LATTE-001-REG"}' \
localhost:50053 pos.v1.CatalogService/CreateVariant
# Set price (amount in smallest currency unit, e.g., 2500000 = Rp 25,000)
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"variant_id": "<variant_id>", "amount": "2500000", "currency": "IDR"}' \
localhost:50053 pos.v1.CatalogService/SetPrice
5. Create an order, add items, and finalize
# Create a draft order
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{}' \
localhost:50053 pos.v1.SalesService/CreateOrder
# Add a line item
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"order_id": "<order_id>", "product_id": "<product_id>", "variant_id": "<variant_id>", "quantity": 2}' \
localhost:50053 pos.v1.SalesService/AddOrderItem
# Finalize the order (snapshots prices, calculates taxes, transitions to OPEN)
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"order_id": "<order_id>"}' \
localhost:50053 pos.v1.SalesService/FinalizeOrder
6. Add payment and complete
# Add a cash payment
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"order_id": "<order_id>", "payment_type": "CASH", "amount": "5000000"}' \
localhost:50053 pos.v1.SalesService/AddPayment
# Complete the order
grpcurl -plaintext \
-H "X-Dev-Tenant-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-User-Id: 00000000-0000-0000-0000-000000000001" \
-H "X-Dev-Membership-Id: 00000000-0000-0000-0000-000000000001" \
-d '{"order_id": "<order_id>"}' \
localhost:50053 pos.v1.SalesService/CompleteOrder
Pagination
List endpoints accept an optional PaginationRequest and return a PaginationResponse.
| Field | Type | Description |
|---|---|---|
page_size | int32 | Number of items to return. Default: 50. Max: 100. |
page_token | string | Opaque cursor (UUID of last item from previous page). Omit for first page. |
| Field | Type | Description |
|---|---|---|
next_page_token | string | Token for the next page. Empty when no more results. |
total_count | int32 | Number of items returned in this page. |
Error Codes
| gRPC Code | Domain Cause | Description | Retry? |
|---|---|---|---|
NOT_FOUND | Resource does not exist | The requested entity was not found by ID. | No |
ALREADY_EXISTS | Duplicate resource / unique constraint | A resource with the given natural key already exists. | No |
INVALID_ARGUMENT | Validation failure, bad UUID | The request contains an invalid or missing field. | No (fix request) |
FAILED_PRECONDITION | Invalid state transition | The operation cannot be performed in the current state (e.g., finalizing a non-DRAFT order). | No (resolve precondition) |
RESOURCE_EXHAUSTED | Rate limit exceeded | The per-IP rate limiter rejected the request. | Yes (backoff) |
UNAUTHENTICATED | Missing or invalid credentials | No valid JWT or API key was provided. | No (fix credentials) |
INTERNAL | Unexpected server error | An unhandled error occurred. | Yes (with backoff) |
Key Concepts
Multi-tenancy
Every request requires a tenant_id (from JWT claims or dev headers). All data is strictly tenant-scoped. Cross-tenant data access is impossible by design.
Money as Integer
All monetary values are stored as int64 in the smallest currency unit (e.g., cents for USD, rupiah for IDR). The field amount: 2500000 with currency: "IDR" represents Rp 25,000. This eliminates floating-point rounding errors.
Order Lifecycle
DRAFT ──(FinalizeOrder)──> OPEN ──(CompleteOrder)──> COMPLETED
│ │
└──(VoidOrder)──> VOIDED <─┘
- DRAFT: Items can be added, removed, and modified. No prices are locked.
- OPEN: Prices, taxes, and modifiers are snapshotted. Payments can be added.
- COMPLETED: Fully paid order. Terminal state.
- VOIDED: Cancelled order (requires reason). Terminal state.
Idempotency
All write operations are safe to retry. POS uses DB constraints for natural idempotency and idempotency keys for request-level deduplication. Payment charges accept an idempotency_key to prevent duplicate charges.