Skip to main content

IDaaS — Integration Guide

Audience: Engineers integrating a client application with the IDaaS platform.
Base URL: https://<your-idaas-host>/api (all paths below omit this prefix)
Interactive docs: GET /swagger-ui.html · Machine-readable spec: GET /v3/api-docs

Table of Contents

  1. What IDaaS Is
  2. Core Concepts
  3. How the Platform Works End to End
  4. Quick-Start Checklist
  5. Step 1 — Register Your Application ← includes appHandle
  6. Step 2 — Authenticate (Get a Token)
  7. Step 3 — Create Subjects and Tags
  8. Step 4 — Initiate a Payment
  9. Step 5 — Receive and Handle Webhooks
  10. Step 6 — Accept or Reject a Transaction
  11. Step 7 — Monitor Webhooks and Delivery
  12. Step 8 — Wallet and Ledger
  13. Step 9 — Settlement
  14. Payload Encryption (JWE)
  15. Webhook Signature Verification
  16. Idempotency
  17. Rate Limiting
  18. Cross-App Tag Claims and Consent
  19. Namespaced Tags — Onboarding Existing Tag Systemsnew
  20. Tag Federation
  21. Error Handling
  22. API Reference Summary
  23. Integration Flows — Sequence Diagrams

1. What IDaaS Is

IDaaS (Identity as a Service) is a multi-tenant platform that provides:
CapabilityWhat it means for your app
Identity tagsEvery user gets a portable handle (e.g. alice) scoped to your app’s namespace, forming a globally unique qualified address (alice@walletapp) that works across all apps on the platform
Cross-app paymentsYour app can send money to a tag on any other app, and receive money from any tag, without needing a direct integration with the other app
Consent-based claimsIf two apps both know the same user, either can request a verified link (claim) between the user’s tags via owner-approved consent
Application walletsIDaaS maintains a single escrow wallet per application that tracks its net monetary position from all completed transactions
Double-entry ledgerEvery accepted transaction creates immutable DEBIT + CREDIT ledger entries with opening/closing balances
SettlementA scheduled end-of-day job (23:59 UTC) produces per-application net position reports for financial reconciliation
JWE encryptionEvery request body and webhook payload can be encrypted end-to-end using EC P-256 keypairs
Your application communicates with IDaaS exclusively via HTTPS REST APIs. All side-effects (webhook dispatch, audit logging) happen asynchronously via Kafka so API calls remain fast.

2. Core Concepts

Application

The top-level entity in IDaaS — it represents your service. Each application gets:
  • A clientId and clientSecret for authentication
  • A webhookSecret for verifying inbound webhook signatures
  • An EC P-256 keypair for payload encryption
  • One escrow wallet for net monetary position tracking

Subject

A user identity within your application. A subject maps your internal user ID (externalId) to the IDaaS system. One subject can own many tags.

Tag

A portable identity handle (e.g. alice, shop-main) scoped to the creating application. Each application has a short, unique appHandle slug (e.g. walletapp). The tag’s globally unique identifier is the qualified address: localTag@appHandle (e.g. alice@walletapp). This means two different applications can each have a user named alice without any conflict — they become alice@walletapp and alice@shopapp, clearly distinct identities. Tag rules:
  • localTag part: 3–64 characters, lowercase alphanumeric + hyphens, not starting or ending with a hyphen
  • appHandle part: 3–30 characters, lowercase alphanumeric + hyphens, not starting or ending with a hyphen; set at registration time and immutable
  • Tags can be resolved publicly using either the bare local name (alice) or the qualified address (alice@walletapp)
  • A bare name lookup that matches tags in more than one application returns 409 Conflict — use the qualified address in that case
  • Can receive payments from any other tag on any application

Transaction

A cross-application payment from one tag to another. Transactions go through a two-step lifecycle — the sending application initiates, the receiving application explicitly accepts or rejects.

Wallet

IDaaS maintains one ApplicationWallet per application. It tracks the cumulative credits and debits from all completed transactions. This is an escrow/settlement wallet, not an end-user wallet — per-user balances remain your application’s responsibility.

Webhook

An outbound HTTP POST from IDaaS to your application notifying you of transaction events. There are two distinct notifications per transaction:
  • RECEIVER webhook — sent to the receiving app’s transactionWebhookUrl when a transaction is addressed to one of its tags
  • SENDER callback — sent to the callbackUrl provided by the sending app when the transaction outcome is determined

3. How the Platform Works End to End

  App A (sender)              IDaaS                    App B (receiver)
  ──────────────              ─────                    ────────────────
  
  [Registration]
  POST /v1/applications  ───►  Create app, generate
                               keypair, wallet          
                        ◄───  clientId, clientSecret,
                               webhookSecret, keys
  
  [Auth]
  POST /v1/auth/token    ───►  Validate credentials
                        ◄───  JWT Bearer token
  
  [Subject + Tag setup]
  POST /v1/subjects      ───►  Create user record
  POST /v1/tags          ───►  Create tag "alice"
  
  [Payment initiation]
  POST /v1/transactions  ───►  Validate tags
  (senderTag=alice@walletapp,    Create txn (AWAITING_ACCEPTANCE)
   receiverTag=bob@shopapp,      Persist RECEIVER webhook record
   amount=500)                  Publish to Kafka
                        ◄───  TXN-xxx reference

                                      │ Kafka consumer (async)

                               POST App B's webhookUrl ───► App B
                               {event: TRANSACTION_INITIATED,  ◄─── 200 OK
                                transaction: {...}}
  
  [Acceptance]
                                                       POST /v1/transactions/TXN-xxx/accept
                                               ◄────  (App B authenticates with its own token)
                               Debit App A wallet
                               Credit App B wallet
                               Write 2 LedgerEntry rows
                               txn.status = COMPLETED
                               Persist SENDER webhook record
                               Publish sender callback to Kafka
                        ◄───  COMPLETED transaction
  
                                      │ Kafka consumer (async)

                               POST App A's callbackUrl ───► App A
                               {event: TRANSACTION_COMPLETED,  ◄─── 200 OK
                                transaction: {...}}
  
  [Daily at 23:59 UTC]
                               Aggregate LedgerEntry rows
                               by wallet for today
                               Write SettlementRecord +
                               per-app SettlementEntry rows
                               Publish idaas.settlements event

4. Quick-Start Checklist

  • Register your application (POST /v1/applications) — save clientSecret, webhookSecret, and key material; they are shown only once
  • Authenticate (POST /v1/auth/token) — obtain a Bearer JWT
  • Provision wallet (POST /v1/wallet) — idempotent, safe to call on every startup
  • Create subjects for your users (POST /v1/subjects)
  • Create tags for your users (POST /v1/tags)
  • Expose a webhook endpoint at your transactionWebhookUrl to receive TRANSACTION_INITIATED events
  • Verify webhook signatures using X-IDaaS-Signature on every inbound webhook
  • Respond with 2xx within 10 seconds on webhook delivery; IDaaS retries 3 times
  • Set up a callback endpoint for TRANSACTION_COMPLETED / TRANSACTION_REJECTED / TRANSACTION_EXPIRED notifications

5. Step 1 — Register Your Application

Registration is public (no auth required). Each application registration creates an independent tenant.

Request

POST /v1/applications
Content-Type: application/json

{
  "name": "WalletApp",
  "appHandle": "walletapp",
  "transactionWebhookUrl": "https://wallet-app.example.com/idaas/webhook",
  "redirectUris": "https://wallet-app.example.com/auth/callback",
  "metadata": "{\"env\": \"production\"}"
}
FieldRequiredDescription
nameHuman-readable application name (2–150 chars)
appHandleImmutable namespace slug for your tags (3–30 chars, lowercase alphanumeric + hyphens, no leading/trailing hyphen)
transactionWebhookUrlRecommendedYour HTTPS endpoint that IDaaS POSTs TRANSACTION_INITIATED events to
redirectUrisOptionalSpace-separated OAuth2 redirect URIs
metadataOptionalFree-form JSON string for your own use

Response

{
  "success": true,
  "message": "Application registered successfully. Store the clientSecret/webhookSecret/appPrivateJwk – they are shown only once.",
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "name": "WalletApp",
    "appHandle": "walletapp",
    "clientId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "clientSecret": "raw-secret-shown-only-once",
    "active": true,
    "transactionWebhookUrl": "https://wallet-app.example.com/idaas/webhook",
    "webhookSecret": "raw-webhook-secret-shown-only-once",
    "appPublicKeyJwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." },
    "appPrivateJwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "...", "d": "..." },
    "encryptionEnabled": true,
    "keyVersion": 1,
    "createdAt": "2025-03-04T10:00:00Z"
  }
}
⚠️ Store immediately and securely:
  • clientSecret — used to obtain Bearer tokens; never shown again
  • webhookSecret — used to verify X-IDaaS-Signature on every inbound webhook; never shown again
  • appPrivateJwk — your application’s private EC key (contains d); required to decrypt JWE-encrypted webhook payloads; never shown again
  • appHandle — returned in the response and in GET /v1/applications/{id}; it is the namespace for all your tags (e.g. alice@walletapp). Immutable.
Lose any of these and you must rotate (use key/webhook secret rotation endpoints, or deactivate + re-register).

6. Step 2 — Authenticate (Get a Token)

All protected endpoints require a Bearer JWT. Tokens expire after 1 hour (configurable via IDAAS_APP_TOKEN_TTL).

Request (form-encoded)

POST /v1/auth/token
Content-Type: application/x-www-form-urlencoded

client_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890&client_secret=raw-secret-shown-only-once

Request (JSON body)

POST /v1/auth/token
Content-Type: application/json

{
  "client_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "client_secret": "raw-secret-shown-only-once"
}

Response

{
  "success": true,
  "message": "Token issued",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIzZmE4NW...",
    "expiresAt": 1741082400,
    "subjectId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  }
}

Using the token

Add the JWT to every subsequent request:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Rate limit: 10 requests / minute per IP. Implement token caching — refresh proactively before expiry rather than waiting for a 401.

Token refresh pattern

┌─────────────────────────────────────┐
│  App startup                        │
│  1. POST /v1/auth/token             │
│  2. Cache token + expiresAt         │
│  3. Schedule refresh at             │
│     (expiresAt - 5 minutes)         │
│                                     │
│  On every API call:                 │
│  if now > cached_expiry - 60s:      │
│    refresh token before call        │
└─────────────────────────────────────┘

7. Step 3 — Create Subjects and Tags

7.1 Create a Subject

A subject represents one of your users inside IDaaS. Create a subject for every user who needs a tag.
POST /v1/subjects
Authorization: Bearer <token>
Content-Type: application/json

{
  "externalId": "user-12345",
  "displayName": "Alice Adeyemi",
  "email": "alice@example.com",
  "metadata": "{\"tier\": \"premium\"}"
}
FieldRequiredDescription
externalIdYour internal user ID (max 255 chars) — must be unique within your app
displayNameOptionalShown on consent pages (max 150 chars)
emailOptionalUsed for consent notification delivery (max 200 chars)
metadataOptionalFree-form JSON string
{
  "success": true,
  "data": {
    "id": "c1d2e3f4-0000-1111-2222-aabbccddeeff",
    "externalId": "user-12345",
    "displayName": "Alice Adeyemi",
    "email": "alice@example.com",
    "createdAt": "2025-03-04T10:01:00Z"
  }
}
Save the returned id (IDaaS subject UUID) — you need it to create a tag.

7.2 Create a Tag

POST /v1/tags
Authorization: Bearer <token>
Content-Type: application/json

{
  "tag": "alice",
  "subjectId": "c1d2e3f4-0000-1111-2222-aabbccddeeff"
}
FieldRequiredValidation
tag3–64 chars; lowercase alphanumeric + hyphens; must not start or end with a hyphen
subjectIdIDaaS subject UUID — must belong to your application
IDaaS automatically derives the qualified address by appending your application’s appHandle: alicealice@walletapp.
{
  "success": true,
  "data": {
    "id": "eeee1111-2222-3333-4444-555566667777",
    "tagString": "alice",
    "qualifiedAddress": "alice@walletapp",
    "status": "ACTIVE",
    "canonicalOwner": {
      "id": "c1d2e3f4-0000-1111-2222-aabbccddeeff",
      "displayName": "Alice Adeyemi"
    },
    "createdByAppId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "createdByAppName": "WalletApp",
    "createdAt": "2025-03-04T10:02:00Z"
  }
}
The qualifiedAddress (alice@walletapp) is the canonical cross-platform identifier for this tag. Share it with other applications so they can address payments to it unambiguously.

7.3 Resolve Any Tag (Public, Cached)

Any application can look up any tag — no auth required. Results are cached for 30 seconds. You can resolve by bare local name or by qualified address:
GET /v1/tags/alice@walletapp
GET /v1/tags/alice
If alice exists on more than one application, the bare-string lookup returns 409 Conflict with the list of qualified addresses. Use the qualified address to avoid ambiguity.
{
  "success": true,
  "data": {
    "tagString": "alice",
    "qualifiedAddress": "alice@walletapp",
    "status": "ACTIVE",
    "canonicalOwner": { "displayName": "Alice Adeyemi" },
    "createdByAppName": "WalletApp"
  }
}
Use this before initiating a payment to confirm the destination tag exists and is ACTIVE.

8. Step 4 — Initiate a Payment

The sending application calls this endpoint to begin a cross-application payment.
Tag addressing: Both senderTag and receiverTag accept either a bare local name (alice) or a qualified address (alice@walletapp).
Use the qualified address whenever possible — bare names that exist on more than one application return a 409 Conflict asking you to specify the qualified address.
POST /v1/transactions
Authorization: Bearer <App A token>
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

{
  "senderTag": "alice@walletapp",
  "receiverTag": "bob@shopapp",
  "amount": 1500.00,
  "currency": "NGN",
  "narration": "Payment for order #99",
  "callbackUrl": "https://wallet-app.example.com/idaas/callback"
}
FieldRequiredDescription
senderTagBare name or qualified address. Must belong to the calling application (403 otherwise). Prefer qualified address (alice@walletapp).
receiverTagBare name or qualified address on any application. Prefer qualified address (bob@shopapp).
amountPositive, up to 15 integer + 4 decimal digits (e.g. 1500.0000)
currencyOptional3-letter ISO 4217 code; defaults to NGN
narrationOptionalPayment note shown to both parties (max 500 chars)
callbackUrlOptionalYour HTTPS URL for the outcome callback — overrides your registered transactionWebhookUrl for this specific transaction’s sender callback. If omitted, IDaaS falls back to the sending application’s registered transactionWebhookUrl.
Idempotency-Key headerRecommendedUnique string (UUID v4 recommended, max 64 chars); duplicate calls within 24 hours return the original response

Response

{
  "success": true,
  "message": "Transaction initiated – awaiting acceptance by receiving application",
  "data": {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "reference": "TXN-9812345678-A1B2C3D4",
    "senderTag": "alice@walletapp",
    "senderAppName": "WalletApp",
    "senderAppId": "3fa85f64-...",
    "receiverTag": "bob@shopapp",
    "receiverAppName": "ShopApp",
    "receiverAppId": "7a9b1c2d-...",
    "amount": 1500.0000,
    "currency": "NGN",
    "status": "AWAITING_ACCEPTANCE",
    "narration": "Payment for order #99",
    "expiresAt": "2025-03-05T10:03:00Z",
    "createdAt": "2025-03-04T10:03:00Z"
  }
}
Transaction responses always return qualified addresses in senderTag and receiverTag (e.g. alice@walletapp, bob@shopapp).

What happens next

  1. IDaaS persists the transaction in AWAITING_ACCEPTANCE state
  2. IDaaS persists a TransactionWebhook record (direction=RECEIVER, status=PENDING)
  3. IDaaS publishes a WebhookDispatchEvent to the idaas.webhooks Kafka topic
  4. The WebhookDispatchConsumer POSTs a TRANSACTION_INITIATED notification to the receiver app’s transactionWebhookUrl (asynchronously)
  5. The transaction auto-expires after 24 hours if not accepted

9. Step 5 — Receive and Handle Webhooks

IDaaS POSTs notifications to your transactionWebhookUrl. You must:
  1. Expose a publicly reachable HTTPS endpoint
  2. Verify the X-IDaaS-Signature header (see §15)
  3. Return 2xx within 10 seconds; IDaaS retries up to 3 times (after 1 s and 2 s back-off)

9.1 RECEIVER Webhook — TRANSACTION_INITIATED

Sent to the receiving application when a new payment is addressed to one of its tags.
POST https://shop-app.example.com/idaas/webhook
Content-Type: application/json
X-IDaaS-Signature: v0=3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4
X-IDaaS-Timestamp: 1741082580
Content-Encryption: JWE   ← only present if encryptionEnabled=true

{
  "event": "TRANSACTION_INITIATED",
  "transaction": {
    "reference": "TXN-9812345678-A1B2C3D4",
    "senderTag": "alice@walletapp",
    "senderAppId": "3fa85f64-...",
    "senderAppName": "WalletApp",
    "receiverTag": "bob@shopapp",
    "receiverAppId": "7a9b1c2d-...",
    "receiverAppName": "ShopApp",
    "amount": 1500.0000,
    "currency": "NGN",
    "narration": "Payment for order #99",
    "expiresAt": "2025-03-05T10:03:00Z",
    "createdAt": "2025-03-04T10:03:00Z"
  }
}
What to do when you receive this:
  1. Verify the signature (see §15)
  2. Parse the reference — this is the key you’ll use to accept or reject
  3. Apply your own business logic (check funds, validate the order, etc.)
  4. Call POST /v1/transactions/{reference}/accept or .../reject within 24 hours
  5. Return 200 OK immediately — do your business logic asynchronously if needed

9.2 SENDER Callback — Outcome Notification

Sent to the sending application’s transaction-specific callbackUrl when provided; otherwise IDaaS falls back to the sending application’s registered transactionWebhookUrl after the transaction outcome is determined.
POST https://wallet-app.example.com/idaas/callback
Content-Type: application/json
X-IDaaS-Signature: v0=abc123...
X-IDaaS-Timestamp: 1741082700

{
  "event": "TRANSACTION_COMPLETED",
  "transaction": {
    "reference": "TXN-9812345678-A1B2C3D4",
    "senderTag": "alice@walletapp",
    "receiverTag": "bob@shopapp",
    "amount": 1500.0000,
    "currency": "NGN",
    "narration": "Payment for order #99",
    "expiresAt": "2025-03-05T10:03:00Z",
    "createdAt": "2025-03-04T10:03:00Z"
  }
}
event valueMeaning
TRANSACTION_COMPLETEDReceiver accepted; wallets have been debited/credited
TRANSACTION_REJECTEDReceiver rejected; no money moved
TRANSACTION_EXPIRED24-hour window elapsed without acceptance; no money moved
TRANSACTION_UPDATEDIntermediate state change (rare)

9.3 Webhook Endpoint Implementation Checklist

✅ Respond with 2xx within 10 seconds
✅ Verify X-IDaaS-Signature before processing (see §15)
✅ Reject if |now - X-IDaaS-Timestamp| > 5 minutes (replay protection)
✅ Make your handler idempotent — webhooks may be delivered more than once
✅ Use the `reference` field as your idempotency key
✅ Dequeue and process asynchronously for slow operations
✅ If encryptionEnabled=true, decrypt the JWE body first (see §14)

10. Step 6 — Accept or Reject a Transaction

Only the receiving application can accept or reject. IDaaS enforces this — calling accept/reject with the wrong token returns 403.

Accept

POST /v1/transactions/TXN-9812345678-A1B2C3D4/accept
Authorization: Bearer <App B (receiver) token>
No request body required. On success:
  • Sender’s wallet is debited by amount
  • Receiver’s wallet is credited by amount
  • Two immutable LedgerEntry rows are written
  • Transaction status moves to COMPLETED
  • Sender’s callbackUrl receives TRANSACTION_COMPLETED (or the sender app’s transactionWebhookUrl if no transaction-specific callback was supplied)
{
  "success": true,
  "message": "Transaction accepted",
  "data": {
    "reference": "TXN-9812345678-A1B2C3D4",
    "status": "COMPLETED",
    "acceptedAt": "2025-03-04T10:10:00Z",
    ...
  }
}

Reject

POST /v1/transactions/TXN-9812345678-A1B2C3D4/reject
Authorization: Bearer <App B (receiver) token>
Content-Type: application/json

{
  "reason": "Insufficient items in stock"
}
The reason field is optional (max 500 chars). On success:
  • No ledger movement
  • Transaction status moves to REJECTED
  • Sender’s callbackUrl receives TRANSACTION_REJECTED (or the sender app’s transactionWebhookUrl if no transaction-specific callback was supplied)

Error responses for accept/reject

StatusMeaning
403Your application is not the receiver of this transaction
404Transaction reference not found
409Transaction is not in AWAITING_ACCEPTANCE state (already completed, rejected, or expired)

11. Step 7 — Monitor Webhooks and Delivery

Check delivery status for a transaction

GET /v1/transactions/TXN-9812345678-A1B2C3D4/webhooks
Authorization: Bearer <token>
Returns up to two records — one RECEIVER and one SENDER:
{
  "data": [
    {
      "id": "wh-uuid-receiver",
      "direction": "RECEIVER",
      "eventType": "TRANSACTION_INITIATED",
      "targetUrl": "https://shop-app.example.com/idaas/webhook",
      "status": "DELIVERED",
      "attempts": 1,
      "deliveredAt": "2025-03-04T10:03:05Z"
    },
    {
      "id": "wh-uuid-sender",
      "direction": "SENDER",
      "eventType": "TRANSACTION_COMPLETED",
      "targetUrl": "https://wallet-app.example.com/idaas/callback",
      "status": "DELIVERED",
      "attempts": 1,
      "deliveredAt": "2025-03-04T10:10:02Z"
    }
  ]
}

Webhook delivery statuses

StatusMeaning
PENDINGQueued, not yet attempted or in progress
DELIVEREDHTTP 2xx received from your endpoint
FAILEDAll 3 attempts exhausted; event dead-lettered to idaas.webhooks.dlt

Find all failed webhooks for your app

GET /v1/webhooks/failed?page=0&size=20
Authorization: Bearer <token>
Use this for operational monitoring — set up an alert if this list is non-empty.

12. Step 8 — Wallet and Ledger

Provision wallet (call on startup)

POST /v1/wallet
Authorization: Bearer <token>
Idempotent — safe to call on every application start.

Get current balance

GET /v1/wallet
Authorization: Bearer <token>
{
  "data": {
    "id": "wallet-uuid",
    "applicationName": "WalletApp",
    "currency": "NGN",
    "balance": 45000.0000,
    "totalDebited": 5000.0000,
    "totalCredited": 50000.0000,
    "updatedAt": "2025-03-04T10:10:00Z"
  }
}
balance = totalCredited − totalDebited. A positive balance means you have net received more than you sent. A negative balance means you have net sent more than you received.

Get ledger statement (paginated)

GET /v1/wallet/statement?page=0&size=20
Authorization: Bearer <token>
{
  "data": {
    "content": [
      {
        "id": "entry-uuid",
        "transactionReference": "TXN-9812345678-A1B2C3D4",
        "entryType": "CREDIT",
        "amount": 1500.0000,
        "currency": "NGN",
        "openingBalance": 43500.0000,
        "closingBalance": 45000.0000,
        "description": "Credit from alice (WalletApp) to bob (ShopApp) — NGN 1500.0000",
        "createdAt": "2025-03-04T10:10:00Z"
      }
    ],
    "totalElements": 47,
    "totalPages": 3
  }
}
entryTypeMeaning
CREDITMoney came in (you were the receiver)
DEBITMoney went out (you were the sender)

13. Step 9 — Settlement

IDaaS automatically runs an end-of-day settlement job at 23:59 UTC every day. It aggregates all completed transactions for the day and produces per-application net positions.

Get today’s settlement (after 23:59 UTC)

GET /v1/settlement/date/2025-03-04
Authorization: Bearer <token>
{
  "data": {
    "id": "settlement-uuid",
    "settlementDate": "2025-03-04",
    "currency": "NGN",
    "totalCredited": 250000.0000,
    "totalDebited": 250000.0000,
    "transactionCount": 183,
    "applicationCount": 7,
    "status": "COMPLETED",
    "entries": [
      {
        "applicationId": "3fa85f64-...",
        "applicationName": "WalletApp",
        "totalCredited": 85000.0000,
        "totalDebited": 12000.0000,
        "netAmount": 73000.0000,
        "openingBalance": 45000.0000,
        "closingBalance": 118000.0000,
        "transactionCount": 52
      }
    ]
  }
}

Manual trigger (operational use)

POST /v1/settlement/run?date=2025-03-04
Authorization: Bearer <token>
Idempotent — if the date is already settled, returns the existing record.

14. Payload Encryption (JWE)

When encryptionEnabled=true for your application, all request bodies must be JWE-encrypted and all webhook payloads you receive will be JWE-encrypted. This provides end-to-end payload confidentiality on top of TLS.

Algorithm

ParameterValue
Key agreementECDH-ES+A256KW
Content encryptionA256GCM
Key typeEC P-256 (secp256r1)

14.1 Encrypting Requests Sent TO IDaaS

1. Fetch the IDaaS public key (cache it)
2. Serialize your JSON request body to a string
3. Encrypt using JWE (alg=ECDH-ES+A256KW, enc=A256GCM) with the IDaaS public key
4. Set headers:
      Content-Encryption: JWE
      Content-Type: application/json    ← describes the decrypted type
5. Send the JWE compact string as the request body
Fetch the IDaaS public key (no auth required):
GET /v1/keys/idaas
{
  "data": {
    "algorithm": "ECDH-ES+A256KW",
    "encAlgorithm": "A256GCM",
    "curve": "P-256",
    "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." },
    "publicKeyBase64": "<Base64 X.509 DER>",
    "usage": "Encrypt request payloads sent TO IDaaS. Set 'Content-Encryption: JWE' header."
  }
}
Example (Java with Nimbus JOSE+JWT):
// 1. Load IDaaS public key from GET /v1/keys/idaas
ECKey idaasPublicKey = ECKey.parse(jwkJson);

// 2. Build JWE
JWEHeader header = new JWEHeader.Builder(JWEAlgorithm.ECDH_ES_A256KW, EncryptionMethod.A256GCM)
    .contentType("application/json")
    .build();

JWEObject jwe = new JWEObject(header, new Payload(plainJsonBody));
jwe.encrypt(new ECDHEncrypter(idaasPublicKey));

String jweCompact = jwe.serialize();

// 3. Send
HttpRequest request = HttpRequest.newBuilder()
    .POST(HttpRequest.BodyPublishers.ofString(jweCompact))
    .header("Content-Type", "application/json")
    .header("Content-Encryption", "JWE")
    .header("Authorization", "Bearer " + token)
    .build();
Example (Node.js with jose library):
import { CompactEncrypt, importSPKI } from 'jose';

// 1. Load IDaaS public key
const publicKey = await importSPKI(idaasPemKey, 'ECDH-ES+A256KW');

// 2. Encrypt
const jweCompact = await new CompactEncrypt(
  new TextEncoder().encode(JSON.stringify(requestBody))
)
  .setProtectedHeader({ alg: 'ECDH-ES+A256KW', enc: 'A256GCM', cty: 'application/json' })
  .encrypt(publicKey);

// 3. Send
await fetch('/v1/transactions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Content-Encryption': 'JWE',
    'Authorization': `Bearer ${token}`,
  },
  body: jweCompact,
});
Example (Python with joserfc):
from joserfc import jwe
from joserfc.jwk import ECKey
import json

# 1. Load IDaaS public key
idaas_key = ECKey.import_key(jwk_dict)

# 2. Encrypt
protected = {"alg": "ECDH-ES+A256KW", "enc": "A256GCM", "cty": "application/json"}
token = jwe.encrypt_compact(protected, json.dumps(body).encode(), idaas_key)

# 3. Send
requests.post(
    '/v1/transactions',
    data=token,
    headers={
        'Content-Type': 'application/json',
        'Content-Encryption': 'JWE',
        'Authorization': f'Bearer {access_token}',
    }
)

14.2 Decrypting Webhooks Received FROM IDaaS

When Content-Encryption: JWE is present on an inbound webhook, the body is a JWE compact string encrypted with your application’s EC public key. Decrypt it with your private key (appPrivateJwk) received once at registration/rotation.
// 1. Detect encrypted webhook
String contentEncryption = request.getHeader("Content-Encryption");
if ("JWE".equals(contentEncryption)) {
    // 2. Load your private key (stored securely at registration time)
    ECKey appPrivateKey = ECKey.parse(storedPrivateKeyJson).toECKey();

    // 3. Decrypt
    JWEObject jwe = JWEObject.parse(request.getBody());
    jwe.decrypt(new ECDHDecrypter(appPrivateKey));
    String plainJson = jwe.getPayload().toString();

    // 4. Process plain JSON payload
    WebhookPayload payload = objectMapper.readValue(plainJson, WebhookPayload.class);
}

14.3 Key Rotation

Rotate your keypair periodically or after a key compromise:
POST /v1/applications/keys/rotate
Authorization: Bearer <token>
Returns a new appPublicKeyJwk and appPrivateJwkstore the private JWK immediately, it is shown only once. After rotation:
  • IDaaS encrypts all subsequent webhooks with the new public key
  • Requests encrypted with the old public key will be rejected
  • Allow a brief overlap window for in-flight messages before destroying the old private key

14.4 Webhook Secret Rotation

If your application loses its webhook secret (or suspects compromise), rotate it without re-registering:
POST /v1/applications/webhook-secret/rotate
Authorization: Bearer <token>
Returns a new webhookSecretstore it immediately, it is shown only once. After rotation:
  • IDaaS signs all subsequent webhooks with the new secret
  • Verification with the old secret fails immediately

15. Webhook Signature Verification

Every outbound webhook POST from IDaaS includes two headers:
X-IDaaS-Signature: v0=3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8...
X-IDaaS-Timestamp: 1741082580

Signature algorithm

signed_message = "v0:" + timestamp + ":" + raw_request_body
signature      = HMAC-SHA256(webhookSecret, signed_message)
header_value   = "v0=" + hex(signature)
Where webhookSecret is the raw (unhashed) secret returned at registration. It can also be rotated via POST /v1/applications/webhook-secret/rotate if lost or compromised.

Verification (pseudo-code)

function verifyWebhook(request, webhookSecret):
    timestamp = request.header("X-IDaaS-Timestamp")
    signature = request.header("X-IDaaS-Signature")
    body      = request.rawBody()          # must be raw bytes, not parsed JSON

    # Step 1 – Replay protection
    if abs(now() - int(timestamp)) > 300:  # 5 minutes
        reject(401, "Timestamp too old")

    # Step 2 – Recompute
    message  = "v0:" + timestamp + ":" + body
    expected = "v0=" + hmac_sha256_hex(webhookSecret, message)

    # Step 3 – Constant-time compare (prevents timing attacks)
    if not constant_time_equals(expected, signature):
        reject(401, "Invalid signature")

    # Step 4 – Proceed with processing
    process(body)

Implementation examples

Java:
public boolean verify(String webhookSecret, String timestamp, String signature, String body) {
    long ts = Long.parseLong(timestamp);
    if (Math.abs(Instant.now().getEpochSecond() - ts) > 300) return false;

    String message = "v0:" + ts + ":" + body;
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(webhookSecret.getBytes(UTF_8), "HmacSHA256"));
    String expected = "v0=" + HexFormat.of().formatHex(mac.doFinal(message.getBytes(UTF_8)));

    // Constant-time compare
    return MessageDigest.isEqual(expected.getBytes(UTF_8), signature.getBytes(UTF_8));
}
Node.js:
import crypto from 'crypto';

function verifyWebhook(webhookSecret, timestamp, signature, rawBody) {
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

    const message = `v0:${timestamp}:${rawBody}`;
    const expected = 'v0=' + crypto
        .createHmac('sha256', webhookSecret)
        .update(message)
        .digest('hex');

    // Constant-time compare
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
Python:
import hmac, hashlib, time

def verify_webhook(webhook_secret: str, timestamp: str, signature: str, raw_body: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:
        return False

    message = f"v0:{timestamp}:{raw_body}"
    expected = "v0=" + hmac.new(
        webhook_secret.encode(), message.encode(), hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, signature)

16. Idempotency

POST /v1/transactions supports the Idempotency-Key request header. Supplying the same key within 24 hours returns the original response without creating a duplicate transaction.
POST /v1/transactions
Authorization: Bearer <token>
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

{ ... }
Best practices:
  • Use UUID v4 as idempotency keys
  • Generate the key before the first attempt and store it alongside your order/payment record
  • Re-use the same key on all retries of the same logical payment
  • Do not re-use keys across different payments

17. Rate Limiting

EndpointLimitKey
POST /v1/auth/token10 requests / minPer IP
POST /v1/transactions60 requests / minPer application
All other endpoints300 requests / minPer IP
When the limit is exceeded, IDaaS returns:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 42
X-RateLimit-Remaining: 0

{
  "success": false,
  "message": "Rate limit exceeded. Retry after 42 seconds.",
  "data": null
}
Retry-After — number of seconds until the bucket refills. Read this value and wait at least that long before retrying.
X-RateLimit-Remaining — tokens remaining in the current window; 0 when you are throttled.
Handling 429 in your integration:
On 429:
  1. Read the Retry-After response header (seconds to wait)
  2. If Retry-After is absent, use exponential back-off:
     wait = min(2^attempt * 1000ms, 60000ms) + random(0, 1000ms)
  3. Retry with the same Idempotency-Key (for POST /v1/transactions)

If your application needs to associate one of your users with a tag they own on another application (e.g. bob on ShopApp), you can request a claim through the consent flow.

18.1 Request a Claim

POST /v1/tags/bob/claims
Authorization: Bearer <App A token>
Content-Type: application/json

{
  "subjectId": "your-app-subject-uuid"
}
IDaaS sends a time-limited consent notification to the tag owner. The consent token URL is:
GET /v1/consent/{consentToken}
This is the URL you can present to the tag owner (e.g. via email or in-app notification) so they can approve or deny the claim.
App A requests claim on tag "bob"


IDaaS generates consentToken
IDaaS notifies "bob" (via email if configured)


Bob visits:  GET /v1/consent/{consentToken}
         │   (public endpoint – no auth)

         ├── POST /v1/consent/{consentToken}/approve
         │       └── Returns attestation JWT for App A

         └── POST /v1/consent/{consentToken}/deny
                 └── Claim marked DENIED

18.3 After Approval

On approval, IDaaS returns an attestation JWT:
{
  "token": "eyJ...",
  "expiresAt": 1741254000,
  "tag": "bob",
  "subjectId": "...",
  "consentId": "..."
}
Store this token — it proves your application has an approved link to that tag. You can verify it with the IDaaS public key.

19. Namespaced Tags — Onboarding Existing Tag Systems

The problem

Many applications that integrate IDaaS already run their own tag / username system. When they onboard their users onto IDaaS, tag collisions are inevitable: WalletApp may already have a user alice, and so does ShopApp — completely different people. A single global UNIQUE(tag_string) constraint would block one application from onboarding. IDaaS solves this with qualified addressing.

How it works

Every application registers with a short, unique appHandle slug (e.g. walletapp). Every tag automatically derives a qualified address localTag@appHandle:
AppLocal tagQualified address
WalletApp (appHandle = walletapp)alicealice@walletapp
ShopApp (appHandle = shopapp)alicealice@shopapp
These are different identities sharing a local name — IDaaS tracks them separately with no constraint conflict.

What stays the same for the host app

Your application continues to use its own tag system internally. IDaaS only requires that you onboard the tags you want to be discoverable cross-platform:
  1. Register on IDaaS with your appHandle
  2. Create a Subject for each user: POST /v1/subjects
  3. Create a Tag for each user you want discoverable: POST /v1/tags with {"tag": "alice", "subjectId": "..."}
  4. IDaaS records alice@walletapp as the globally unique qualified address
  5. Other applications discover your users via GET /v1/tags/alice@walletapp
  6. Payments are routed to alice@walletapp — your internal tag system is unaffected
Tags that are not onboarded are simply not discoverable via IDaaS. This is intentional — you control which users participate in the cross-platform network.

Address resolution behaviour

What you sendBehaviour
alice@walletappLooks up by qualified address — fast, unambiguous, always recommended
aliceLooks up by bare local name — succeeds only if exactly one application has a tag named alice; returns 409 Conflict with the list of available qualified addresses if more than one app does

Example: sending a payment to a user on another app

POST /v1/transactions
Authorization: Bearer <WalletApp token>
Content-Type: application/json

{
  "senderTag": "alice@walletapp",
  "receiverTag": "bob@shopapp",
  "amount": 500.00,
  "currency": "NGN"
}
POST /v1/transactions
Authorization: Bearer <WalletApp token>
Content-Type: application/json

{
  "senderTag": "alice",
  "receiverTag": "bob",
  "amount": 500.00,
  "currency": "NGN"
}
The second form works only when alice is unique on the platform and bob is unique on the platform. If either is ambiguous, IDaaS returns 409 and lists the qualified addresses to use.

appHandle rules

RuleDetail
Length3–30 characters
CharactersLowercase letters, digits, hyphens — no spaces or special characters
PositionMust not start or end with a hyphen
UniquenessMust be globally unique across all applications
ImmutabilityCannot be changed after registration. Changing it would break all published qualified addresses.

20. Tag Federation

Federation links a tag to an identity on an external identity provider (e.g. “alice on WalletApp is the same person as alice@github.com”). This is for advanced cross-platform identity scenarios.
POST /v1/tags/alice/federation
Authorization: Bearer <token>
Content-Type: application/json

{
  "provider": "github",
  "externalIdentifier": "alice-github-username"
}
IDaaS issues a challenge; your application must verify ownership on the external provider, then call:
POST /v1/federation/verify/{challenge}
Authorization: Bearer <token>

21. Error Handling

All IDaaS responses use a consistent ApiResponse<T> envelope:
{
  "success": false,
  "message": "Validation failed",
  "errorCode": "IDAAS-VALID-2001",
  "errors": ["amount must be greater than 0", "currency is required"],
  "timestamp": "2026-03-27T11:45:12.772Z"
}
errorCode is the stable machine-readable identifier (for example IDAAS-AUTH-1002). Client applications should branch handling logic by errorCode, not by free-text message.

Complete error code catalog

All IDaaS errors are returned with a stable error code in the errorCode field. Client applications should parse and branch on this code rather than free-text messages, enabling reliable structured error handling.

Authentication & Authorization (1000–1999)

CodeHTTPMessageCauseResolution
IDAAS-AUTH-1001401Authentication requiredMissing/invalid Authorization headerProvide Bearer token: Authorization: Bearer <token>
IDAAS-AUTH-1002401Invalid JWTToken is malformed, expired, or has invalid signatureRe-authenticate via POST /v1/auth/token
IDAAS-AUTH-1003401Token has been revokedApplication was deactivated after token issuanceRe-authenticate with active application credentials
IDAAS-AUTH-1004401Authentication failedClient ID/secret mismatch, malformed Basic authVerify credentials and application is active
IDAAS-AUTH-1005401Application not foundApplication in JWT subject does not existVerify application ID and registration status
IDAAS-AUTH-1006403Access deniedAuthenticated but unauthorized for this operationUse correct role/credentials for operation
IDAAS-AUTH-1007403Transaction is not addressed to the calling applicationReceiver attempting to act on sender’s transactionUse credentials of the receiving application
IDAAS-AUTH-1008403Identity link is not associated with the calling applicationApplication does not own this resourceUse credentials of the owning application

Validation Errors (2000–2999)

CodeHTTPMessageCauseResolution
IDAAS-VALID-2001400Validation failedField-level validation errors (required fields, format, length, etc.)Fix request payload per errors list and retry
IDAAS-VALID-2002400keyword is requiredSearch endpoint called without keyword query paramProvide non-empty keyword parameter
IDAAS-VALID-2003400keyword must be at least 2 charactersSearch keyword is too shortUse keyword with ≥ 2 characters
IDAAS-VALID-2004415Unsupported media typeContent-Type not application/json, or JWE payload misformattedSend JSON with Content-Type: application/json; for JWE set Content-Encryption: JWE
IDAAS-VALID-2005400Decryption failedJWE payload could not be decrypted or is malformedVerify encryption key and payload format

Resource Not Found (3000–3999)

CodeHTTPMessageCauseResolution
IDAAS-RES-3001404Application not foundApplication UUID does not existVerify application UUID; register if needed
IDAAS-RES-3002404Subject not foundSubject UUID does not exist in calling applicationVerify subject ID; create via POST /v1/subjects if needed
IDAAS-RES-3003404Subject not found with external IDSubject with given external ID does not existVerify external ID; create subject if needed
IDAAS-RES-3004404Tag not foundTag (qualified address or bare name) cannot be resolvedVerify tag exists; use qualified address tag@appHandle if bare name is ambiguous
IDAAS-RES-3005404Claim not foundClaim UUID does not existVerify claim ID
IDAAS-RES-3006404Claim not found with consent tokenConsent token is invalid or expiredRequest a new claim to get fresh consent token
IDAAS-RES-3007404IdentityLink not foundIdentity link UUID does not exist or was revokedVerify identity link ID
IDAAS-RES-3008404IdentityLink not found with challenge tokenChallenge token is invalid, expired, or already usedRequest new federated identity challenge
IDAAS-RES-3009404FederatedIdentity not foundNo federated identity exists for this tagCreate federated identity via POST /v1/identities/federate
IDAAS-RES-3010404Transaction not foundTransaction reference does not existVerify transaction reference
IDAAS-RES-3011404ApplicationWallet not foundApplication wallet not auto-provisioned (rare)Contact support; wallet should auto-create on first transaction
IDAAS-RES-3012404Resource not foundInvalid endpoint pathVerify endpoint path matches API documentation

Resource Conflicts (4000–4999)

CodeHTTPMessageCauseResolution
IDAAS-CONF-4001409Tag already existsTag with this qualified address already exists globallyUse different tag name in your application
IDAAS-CONF-4002409Your application already has a tag with this nameTag string already exists in your applicationUse different tag name or reuse existing
IDAAS-CONF-4003409Subject already exists with this external IDSubject with this external ID already exists in appUse unique external ID or fetch existing subject
IDAAS-CONF-4004409Subject already has an active claim on this tagSubject is already linked to tag via active claimRevoke existing claim if you need to re-link
IDAAS-CONF-4005409A pending claim for this subject/tag already existsPending consent request for this subject/tag in progressWait for pending claim to be approved/denied or revoke it

Invalid State / Business Logic (5000–5999)

CodeHTTPMessageCauseResolution
IDAAS-STATE-5001422Cannot claim a disabled tagAttempting to claim a tag with status = DISABLEDEnable tag via PATCH /v1/tags/{tag}/enable or use active tag
IDAAS-STATE-5002422Subject does not belong to the calling applicationSubject is owned by a different applicationUse a subject owned by your application
IDAAS-STATE-5003422Consent token has expiredConsent token is past expiration (default: 48 hours)Request new claim for fresh consent token
IDAAS-STATE-5004422Challenge is no longer validIdentity link challenge in terminal state (USED, DENIED, EXPIRED)Request new federated identity challenge
IDAAS-STATE-5005422Only ACTIVE or PENDING claims can be revokedAttempting to revoke claim in REVOKED or DENIED stateOnly active/pending claims can be revoked
IDAAS-STATE-5006422New owner must have an active claim on this tag before transferTag transfer target does not have active claimEnsure new owner has approved claim before transferring
IDAAS-STATE-5007409Transaction is not in AWAITING_ACCEPTANCE stateAttempting to accept/reject transaction not pendingCheck transaction status; only AWAITING_ACCEPTANCE can be acted on
IDAAS-STATE-5008422Transaction has expiredAttempting to accept transaction past expiration timeTransaction cannot be acted on; check with receiver if needed

Rate Limiting (6000–6999)

CodeHTTPMessageCauseResolution
IDAAS-RATE-6001429Rate limit exceeded. Retry after [N] seconds.Too many requests for this endpoint/IP/appUse Retry-After header for exponential backoff; default limits: 10 req/min on /v1/auth/token, 60 req/min on /v1/transactions, 300 req/min on others

System / Internal Errors (9000–9999)

CodeHTTPMessageCauseResolution
IDAAS-SYS-9001500An unexpected error occurredUnhandled exception in application logicRetry with exponential backoff; contact support if persistent
IDAAS-SYS-9002400JWE decryption errorJWE payload is malformed or uses wrong encryption parametersVerify encryption key and payload format

Error Code Categories

CategoryCode RangeDescription
AUTH1000–1999Authentication & Authorization failures
VALID2000–2999Request validation & bad input
RES3000–3999Resource not found / lookup failures
CONF4000–4999Resource conflicts & duplicates
STATE5000–5999Invalid state transitions & business logic
RATE6000–6999Rate limiting & throttling
SYS9000–9999System/Internal errors
Total: 41 documented error codes – all error responses include errorCode and message.

HTTP status codes

CodeMeaningAction
200 / 201SuccessProcess normally
400Validation errorFix request body; check message for field details
401Missing or invalid tokenRe-authenticate with POST /v1/auth/token
403Forbidden — wrong applicationVerify you’re using the correct token for this operation
404Resource not foundCheck the reference/ID
409Conflict — duplicate or invalid statee.g. tag already exists, transaction already completed
429Rate limit exceededBack off and retry with exponential delay
500Server errorRetry with back-off; contact IDaaS support if persistent

Validation error example

{
  "success": false,
  "message": "Validation failed",
  "errorCode": "IDAAS-VALID-2001",
  "errors": [
    "amount must be greater than zero",
    "senderTag is required"
  ],
  "timestamp": "2026-03-27T11:45:12.772Z"
}

22. API Reference Summary

Base path for all endpoints: /api/v1
All protected endpoints require: Authorization: Bearer <token>

Applications & Authentication

MethodPathAuthDescription
POST/v1/applicationsPublicRegister application — appHandle required; secrets returned once
GET/v1/applicationsBearerList all applications
GET/v1/applications/{id}BearerGet application by UUID (includes appHandle)
PATCH/v1/applications/{id}/deactivateBearerDeactivate application + blacklist tokens
POST/v1/auth/tokenPublicExchange credentials for JWT (rate: 10/min/IP)
POST/v1/applications/keys/rotateBearerRotate EC keypair — new private key returned once
POST/v1/applications/webhook-secret/rotateBearerRotate webhook signing secret — new secret returned once

Encryption Key Management

MethodPathAuthDescription
GET/v1/keys/idaasPublicIDaaS public key (JWK + Base64 DER)

Subjects

MethodPathAuthDescription
POST/v1/subjectsBearerCreate a subject (user)
GET/v1/subjectsBearerList all subjects for calling app
GET/v1/subjects/{id}BearerGet subject by UUID
GET/v1/subjects/{id}/tagsBearerList tags owned by a subject

Tags

MethodPathAuthDescription
POST/v1/tagsBearerCreate a tag (qualified address localTag@appHandle derived automatically)
GET/v1/tagsBearerList tags for calling app
GET/v1/tags/{tag}PublicResolve tag by bare name or qualified address tag@appHandle (cached 30 s; 409 if bare name is ambiguous)
PATCH/v1/tags/{tag}/disableBearerDisable a tag
POST/v1/tags/{tag}/transferBearerTransfer tag ownership
POST/v1/tags/{tag}/claimsBearerRequest a cross-app claim
GET/v1/tags/{tag}/claimsBearerList claims on a tag
POST/v1/tags/{tag}/federationBearerInitiate federation
GET/v1/tags/{tag}/federationBearerGet federation info

Transactions

MethodPathAuthDescription
POST/v1/transactionsBearerInitiate payment; senderTag/receiverTag accept bare name or qualified address (rate: 60/min/app)
GET/v1/transactions/{reference}BearerGet transaction + webhook delivery status
GET/v1/transactions/sentBearerPaginated sent transactions
GET/v1/transactions/receivedBearerPaginated received transactions
GET/v1/transactions/pendingBearerPaginated acceptance queue
POST/v1/transactions/{reference}/acceptBearerAccept (receiver app only)
POST/v1/transactions/{reference}/rejectBearerReject (receiver app only)

Wallet & Ledger

MethodPathAuthDescription
POST/v1/walletBearerProvision wallet (idempotent)
GET/v1/walletBearerGet wallet balance
GET/v1/wallet/statementBearerPaginated ledger statement

Settlement

MethodPathAuthDescription
GET/v1/settlementBearerList settlement batches (paginated)
GET/v1/settlement/{id}BearerGet batch by UUID
GET/v1/settlement/date/{date}BearerGet batch by date (YYYY-MM-DD)
POST/v1/settlement/runBearerManually trigger settlement

Webhooks

MethodPathAuthDescription
GET/v1/transactions/{reference}/webhooksBearerAll webhook records for a transaction
GET/v1/transactions/{reference}/webhooks/receiverBearerRECEIVER webhook record
GET/v1/transactions/{reference}/webhooks/senderBearerSENDER webhook record
GET/v1/webhooks/{id}BearerSingle webhook record by UUID
GET/v1/webhooks/failedBearerAll FAILED webhooks for calling app
MethodPathAuthDescription
GET/v1/consent/{token}PublicView consent request details
POST/v1/consent/{token}/approvePublicApprove claim
POST/v1/consent/{token}/denyPublicDeny claim

Claims

MethodPathAuthDescription
POST/v1/claims/{id}/revokeBearerRevoke an active claim

Federation

MethodPathAuthDescription
POST/v1/federation/verify/{challenge}BearerVerify federation challenge

Actuator

PathAuthDescription
/actuator/healthPublicHealth check (DB + Redis)
/actuator/infoPublicApp info

23. Integration Flows — Sequence Diagrams

Flow A: Two-App Payment (Happy Path)

App A (sender)           IDaaS                    App B (receiver)
──────────────           ─────                    ────────────────

─── Onboarding ──────────────────────────────────────────────────

POST /v1/applications ──► Register App A (appHandle="walletapp")
                    ◄──  clientId, clientSecret, webhookSecret, keys

POST /v1/auth/token ──►  Issue JWT
                    ◄──  Bearer token (1 h)

POST /v1/subjects ────►  Create subject (Alice)
POST /v1/tags ────────►  Create tag "alice" → qualified: alice@walletapp
POST /v1/wallet ──────►  Provision wallet (idempotent)

                                           POST /v1/applications ──► Register App B (appHandle="shopapp")
                                                               ◄──  credentials, keys
                                           POST /v1/auth/token ──►  Issue JWT
                                           POST /v1/subjects ────►  Create subject (Bob)
                                           POST /v1/tags ────────►  Create tag "bob" → qualified: bob@shopapp
                                           POST /v1/wallet ──────►  Provision wallet

─── Payment ─────────────────────────────────────────────────────

POST /v1/transactions ──► Validate alice@walletapp ∈ App A ✓
(senderTag=alice@walletapp, Validate bob@shopapp exists ✓
 receiverTag=bob@shopapp,   Create txn (AWAITING_ACCEPTANCE)
 amount=500)                Persist RECEIVER webhook row
                    ◄──   TXN-xxx (AWAITING_ACCEPTANCE)

                           │ Kafka async

                    POST /idaas/webhook ───────────────────────►
                    {event: TRANSACTION_INITIATED,               200 OK
                     X-IDaaS-Signature: v0=...,            ◄────
                     transaction: {ref, senderTag: alice@walletapp,
                                   receiverTag: bob@shopapp, amount, ...}}
                                                           Verify signature ✓
                                                           Check business logic ✓
                                                           POST /v1/transactions/TXN-xxx/accept
                                                    ◄────  (App B's Bearer token)
                           Debit App A wallet
                           Credit App B wallet
                           Write 2 LedgerEntry rows
                           txn.status = COMPLETED
                           Persist SENDER webhook row
                    ◄──────────────────────────────────────────  COMPLETED

                           │ Kafka async

                    POST App A callbackUrl ──────────────────►
                    {event: TRANSACTION_COMPLETED,              200 OK
                     X-IDaaS-Signature: v0=...}           ◄────
App A: update your
internal order status ✓

Flow B: Transaction Rejected

App A sends TXN-yyy (amount=200)

         ▼ IDaaS webhooks App B: TRANSACTION_INITIATED

                   ▼ App B reviews and decides to reject
         POST /v1/transactions/TXN-yyy/reject
         { "reason": "Service not available" }

                   ▼ IDaaS: txn.status = REJECTED
                     No wallet movement
                     Notify App A: TRANSACTION_REJECTED

                   ▼ App A callbackUrl
         { "event": "TRANSACTION_REJECTED",
           "transaction": { "reference": "TXN-yyy", ... } }

Flow C: Transaction Expired

App A sends TXN-zzz

         ▼ IDaaS webhooks App B: TRANSACTION_INITIATED
         App B does not respond within 24 hours

                   ▼ IDaaS scheduled job (every 5 min)
                     Finds AWAITING_ACCEPTANCE txns past expiresAt
                     Sets txn.status = EXPIRED
                     No wallet movement
                     Notify App A: TRANSACTION_EXPIRED

                   ▼ App A callbackUrl
         { "event": "TRANSACTION_EXPIRED",
           "transaction": { "reference": "TXN-zzz", ... } }

Flow D: Encrypted Payment

App A (encryptionEnabled=true)         IDaaS
──────────────────────────────         ─────

GET /v1/keys/idaas ────────────────►
                   ◄────────────────  IDaaS public key JWK

Serialize request body to JSON
Encrypt with IDaaS public key (JWE)
POST /v1/transactions ─────────────►
  Headers:
    Content-Encryption: JWE
    Authorization: Bearer <token>
  Body: eyJ...JWE compact string...
                                      JweRequestDecryptionFilter:
                                        detects Content-Encryption: JWE
                                        decrypts with IDaaS private key
                                        passes plain JSON to controller
                   ◄────────────────  TXN reference (AWAITING_ACCEPTANCE)

                   │ Kafka async

                   JWE-encrypt payload with App B's EC public key
                   POST App B webhookUrl:
                     Content-Encryption: JWE
                     X-IDaaS-Signature: v0=...
                     Body: eyJ...JWE compact string...

                   App B:
                     Verify signature ✓
                     Detect Content-Encryption: JWE
                     Decrypt with own private key
                     Process plain JSON payload