Retail WalletArchitecture spec Providers Partner API reference

Retail Wallet Platform — Architecture & Capacity Specification

15 September 2026

Section 1Scale assessment

1.1 Assumptions

ParameterMinMaxBasis
Registered users5,000,0005,000,000Embargo
Onboarding of the base417,000 / month100,000 / day5M over 12 months; launch-campaign day
Active cardholders per month250,000500,0005–10% of registered
Card purchases per active cardholder per month816Embargo
Top-ups per active cardholder per month24Embargo
App opens per active cardholder per month1530balance and history checks; 3 API reads per open; no client-side caching assumed
Merchants3,0007,000rollout / full estate

1.2 Traffic directions

Section 2Architecture

AWS eu-west-2 · 3 AZ · VPC private subnets Embargo backend OAuth2 CC · REST Embargo webhook HTTPS · HMAC verify server ↔ server device → provider (inside the SDK) card authorisation WAF + ALB TLS 1.2+ · HC 10 s scheduler · reporting SKIP LOCKED · exports webhook-dispatcher HMAC · backoff ≤24 h · 3→30 api Fargate 2 vCPU · 4→40 events-ingest verify · publish · 3→30 SNS FIFO → SQS FIFO archive 90 d · wallet order ledger-worker Fargate 2 vCPU · 4→40 Redis rate limits · hot balances PostgreSQL RDS r6g.xlarge Multi-AZ dedicated per partner · replica monthly partitions Wallester card accounts · authorisation funds system of record Stripe card top-ups · vault · 3-DS Plaid pay-by-bank · identity Embargo app · RW Mobile SDK card · bank · KYC · wallet sheets Google Wallet · Apple Pay push provisioning · tokens Cardholder at the till authorisation at the processor platform not in the path webhooks in · verified per provider account transfers · top-up · discount · cashback card data · bank auth · tokens Embargo app ↔ Embargo backend · the app never calls the Partner API

2.1 Roles

RoleFunctionStateScales on
apiPartner API (REST). Balances from the event projection (Redis, then PostgreSQL); other reads from the replica; writes to primary. Never calls Wallester to answer a read.noneCPU 60% · requests per target
events-ingestReceives provider webhooks. Verify per provider (Stripe HMAC signature, Plaid JWT, Wallester Basic-auth credentials + source IP allow-list — no payload signature is offered) → publish to SNS FIFO with the provider's event identity as deduplication ID → 200 only after the publish succeeds. No business logic; lasting deduplication happens in PostgreSQL.nonerequests per target
ledger-workerConsumes the SQS FIFO queue subscribed to the topic, in wallet order. Records the event key with INSERT … ON CONFLICT in the same transaction that applies the event to the ledger and computes the reward; provider credits and outbound webhooks are recorded as pending rows in the same transaction (transactional outbox) and executed from those rows. Idempotent.noneSQS queue depth / age
webhook-dispatcherDelivers signed events to Embargo. Exponential backoff to 24 h, then DLQ. 200 concurrent connections per task.noneoutbound queue depth
schedulerCashback confirmation after pending period, slot expiry, report subscriptions. Several active instances: due jobs are claimed with SELECT … FOR UPDATE SKIP LOCKED and their state changes in the same transaction; external actions run from outbox rows after commit.job rows in PostgreSQLdue-job backlog
reportingCSV / JSON exports, bulk merchant imports, KYB orchestration. Replica reads only.nonejob queue depth

2.2 Compute and storage sizing

ComponentSpecificationBaselineAutoscale max
apiFargate 2 vCPU / 4 GB4 tasks40
events-ingestFargate 2 vCPU / 4 GB3 tasks30
ledger-workerFargate 2 vCPU / 4 GB4 tasks40
webhook-dispatcherFargate 1 vCPU / 2 GB3 tasks30
schedulerFargate 1 vCPU / 2 GB2 tasks, both active4
reportingFargate 2 vCPU / 4 GB2 tasks10
PostgreSQL primaryDedicated to the Embargo programme. RDS db.r6g.xlarge 4 vCPU / 32 GB, Multi-AZ (sync standby), gp3 1 TB, storage autoscaling1 + standby→ r6g.4xlarge
PostgreSQL read replicaRDS db.r6g.xlarge13
RedisElastiCache cache.r6g.large 2 vCPU / 13 GB, Multi-AZ, 2 nodes2cluster mode
NetworkALB, WAF, 3 × NAT gateway
Platform servicesSNS FIFO with 90-day archive, SQS FIFO, CloudWatch, Secrets Manager, KMS, ECR, cross-region backups
Total31 vCPU / 62 GB compute · 8 vCPU / 64 GB database · 1 TB storage

2.3 Partner integration surface

SurfaceEmbargo integratesEmbargo does not integrate
ServerPartner API (REST, OAuth2 client credentials) + one webhook endpointWallester, Stripe, Plaid, Google Pay, Apple Pay — no server calls, no keys, no programme membership
AppRetail Wallet Mobile SDK (iOS, Android, React Native): payment, pay-by-bank, KYC and wallet sheetsStripe SDK, Plaid Link, Google TapAndPay, Apple PassKit — wrapped inside our SDK
One-off, administrativeAndroid: package name + SHA-256 signing fingerprint → we register the app in the issuer's Google Pay programme. iOS: in-app provisioning entitlement requested from Apple for Embargo's developer account, with our issuer sponsorship letter

Card data, bank authorisation and wallet tokens travel from the device to the provider inside the SDK; the Embargo app itself talks only to the Embargo backend.

Section 3Data and durability

Section 4Monitoring, alerting, on-call

4.1 Alert thresholds

SignalThresholdAction
API latency p95> 300 ms · 5 minpage
API 5xx rate> 0.5% · 5 minpage
SQS oldest-message age> 30 sautoscale; page at 2 min
Dead-letter queues — SNS-to-SQS delivery and processing≥ 1 messagepage
Archive replayfailed replay or failed deliveryalert the recovery operator
Rate limiting on per-task fallbackRedis unavailablepage
Webhook delivery failure rate> 2% · 10 minautomatic notice to Embargo technical contact
PostgreSQL CPU · free storage · replica lag> 70% · < 20% · > 10 spage
Nightly reconciliationany wallet out of balancepage; incident opened
Provider health (Wallester, Stripe, Plaid)status or error-rate changestatus page; Embargo notified

Section 5Operating targets

MetricTargetMeasurement
Partner API availability99.9% / month (≤ 43 min)ALB 5xx and health-check failures, excluding announced maintenance
API latencyp95 < 150 ms · cached reads < 60 msserver-side, per endpoint; validated in the pre-launch load test
Event delivery to Embargop95 < 5 s from durable receipt by Retail Wallet to acknowledgement by Embargo, in normal operation · retry delivery may take longerdispatcher timestamps; processor → Retail Wallet latency is measured separately
Data lossRPO 0 zone (synchronous standby) · ≤ 5 min region — targetend-to-end recovery drill before launch, then quarterly
Recoveryzone: expected 1–2 min, automatic · region: RTO ≤ 60 min — targetfailover tests; recovery drill
Incident responseto be agreed before launch — proposed P1 ack 15 min · partner notified 30 minon-call tool records once live
Capacity assuranceload test at 10× current peak before go-live and before each doublingk6 output shared with Embargo

Figures are design targets; each is validated by the named test or drill before production launch. Contractual service levels are set in the partnership agreement.

Section 6Questions from Embargo

Server type, OS, count, CPU

Language / framework

Load balancer

Rate limiting

Upstream API limits — the APIs we call

ProviderDocumented limitHow it is signalledOur usage at Embargo's peakHandling
StripeGlobal 100 req / s live (25 in sandbox); individual endpoints 25 req / s; 1,000 updates per PaymentIntent per hour; concurrency limits; read allocation of 500 GET requests per transaction over a rolling 30 days429 with Stripe-Rate-Limited-Reason (global-rate, endpoint-rate, …-concurrency); 429 lock_timeout on concurrent access to one objectCard top-ups ≤ 6 / s at lunch, card linking ~7 / s on a campaign day — about two requests each; reads are webhook-driven, not polledClient-side token bucket under 25 req / s per endpoint; exponential backoff with jitter; mutations on one PaymentIntent serialised; Stripe Support notified before campaign days
PlaidPer client: /payment_initiation/payment/create 240 / min, /payment_initiation/consent/create 100 / min, /identity_verification/create 120 / min (/get 420 / min), /link/token/create 20,000 / min; per-Item limits on data endpoints (e.g. /accounts/get 15 / min per Item)429, error_type RATE_LIMIT_EXCEEDED with a per-endpoint error_code; INSTITUTION_RATE_LIMIT when a bank itself throttlesPay-by-bank top-ups ≈ 3 / s at lunch (≈ 170 / min) against 240 / min; KYC starts ≈ 7 / s on a campaign day (420 / min) against 120 / minOnboarding throughput depends on the agreed Plaid limits: at the default 120 / min a campaign-day KYC rate of 420 / min queues faster than it drains. Raising identity_verification/create and payment/create is a precondition of bulk onboarding; during campaigns requests may be queued and completion times may increase
WallesterNo rate limits and no programme-level or per-product-code ceiling, including for account transfers and push provisioning — API reference, confirmed by Wallester on 14 September 2026. Platform load “typically several hundred RPS, with peaks exceeding 1,000 RPS”, with capacity added automaticallyNo 429: under extreme burst or overload the API returns 500 or 504Account transfers ≈ 13 / s during the cashback payout run, ~1 / s discount returns; card issuing ≈ 7 / s on a campaign day — ≈ 20 / s combined worst case, plus rate-limited balance checks for recently active accounts, scheduled outside peaksPer-provider token bucket with a configurable ceiling regardless; payout transfers batched and spread over the run window; a write is never retried on 500 / 504 without a lookup; calls leave from fixed NAT egress IPs, which Wallester allow-lists

Sources and the full provider profiles (availability, latency, webhooks, idempotency, residency): Provider dependencies. Numbers change; the token-bucket ceilings are configuration.

Idempotency and duplicate handling

Every entry point has one key on which a message is processed at most once at a time; a duplicate replays the original result instead of acting again. Duplicate prevention relies on durable operation identifiers and database constraints; timed-out provider operations remain pending until their outcome is confirmed.

Entry pointKeyWhere enforcedDuplicate outcome
Partner API writes — top-ups, cards, users, merchants, reportsIdempotency-Key header + client_idRedis, 24 h; then a unique constraint on the stored keyOriginal response replayed with the same topup_id / card_id; the stored key prevents a second charge, card or merchant being created
Provider webhooks in — Wallester, Stripe, Plaidthe provider's event identity: Stripe event.id, Plaid payment_id + status, Wallester X-Request-ID, confirmed stable across retries, with (type, data.id) as a second checkSNS FIFO deduplication ID, 5-minute windowAcknowledged with 200; a later copy is a no-op in the consumer
Topic and queue → ledger-workerSNS FIFO → SQS FIFO: MessageDeduplicationId = event_id, MessageGroupId = wallet_idINSERT … ON CONFLICT on processed_events(consumer, event_id) in the same transaction as the ledger change — atomic, no prior SELECTSecond copy is a no-op; one wallet's events apply strictly in order, one at a time
Concurrent writes to one walletwallet_idRow lock (SELECT … FOR UPDATE) inside the transactionSerialised — concurrent debits of one wallet are applied one at a time against the same row
Balance movements at the issuer — top-up load, discount return, cashback payoutour credit_id as the reference on Wallester's account-to-account transfercredits.credit_id unique on our side; the issuer's behaviour on a repeated reference is being confirmedThe transfer is never blind-retried: on timeout or 5xx we look it up by our reference and act on what exists; the credit stays pending until the outcome is confirmed by that lookup or by the issuer's transfer notification
Card creation at the issuerour card_request_id as Wallester's card external_id (“must be unique”)card_request_id unique on our side; the issuer has no idempotency on card creation — a repeated create makes a second cardThe create is never blind-retried: on timeout or 5xx the card is looked up by external_id first; reconciliation flags any user with more than one active card
Outbound webhooks to Embargoevent_id in the payloadAt-least-once with retries; Embargo deduplicates by event_idDuplicate deliveries are expected; the partner deduplicates by event_id, as documented in the API
Scheduled jobs — cashback confirmation, slot expiry, reportsjob key, e.g. cashback_id:confirmSELECT … FOR UPDATE SKIP LOCKED on the job row + unique job keyOne instance claims each job; the others skip it and take the next
Replay from the event archivethe original event_idthe same processed_events constraint, kept for the 90-day archive windowAn event already applied is a no-op; only missing events change state
Bulk merchant importIdempotency-Key for the batch; external_ref per rowUnique per partnerA rerun updates rows, never creates twins

In-flight operations and crash recovery

The case that matters: a process dies between writing to the database, sending a message and calling a provider. The design makes such an operation detectable and resumable rather than assuming it does not happen.

Balances — served from our event projection

Refunds, reversals and cashback

How a Wallester refund references the original purchase is being confirmed with Wallester. After payout the programme rule applies: a reversed cashback is offset against future cashback and is never debited from the card.

Database sharding

Message queues — receiving and processing events

Microservices

What request rate must the platform support?

Transaction capacity

Does Embargo integrate with Google, Apple, Stripe or Plaid?

Monitoring and alerts · failure plan, RPO / RTO · penetration test · SLA

Retail Wallet Platform — Architecture & Capacity Specification · 15 September 2026. Companion: Partner API reference v1.1 · Programme overview.