-
Notifications
You must be signed in to change notification settings - Fork 147
Expand file tree
/
Copy pathopenapi.yaml
More file actions
551 lines (517 loc) · 19 KB
/
Copy pathopenapi.yaml
File metadata and controls
551 lines (517 loc) · 19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
openapi: 3.0.3
info:
title: Aframp Pay API
version: 0.1.0
description: >
Merchant-facing backend for Aframp Pay — Stellar-settled point-of-sale payments.
Prose reference with frontend guidance lives in API.md; this spec is the
machine-readable contract for generating typed clients.
Money is always integer stroops (1 unit = 10,000,000 stroops), never a float.
servers:
- url: http://127.0.0.1:3000
description: Local development
tags:
- name: System
- name: Auth
- name: Wallet
- name: Payment requests
- name: Money
# Either authenticates. The Authorization header wins when both are sent.
security:
- cookieAuth: []
- bearerAuth: []
paths:
/:
get:
tags: [System]
summary: Service name
description: Returns the literal string `aframp` as text/plain, not JSON.
security: []
responses:
'200':
description: OK
content:
text/plain:
schema: { type: string, example: aframp }
/health:
get:
tags: [System]
summary: Liveness probe
security: []
responses:
'204': { description: Service is up (empty body) }
/signup:
post:
tags: [Auth]
summary: Create a user and merchant
description: >
Sets the aframp_session HttpOnly cookie and echoes the same JWT in the
body for API clients. Browsers should rely on the cookie and ignore the
token field — storing it in localStorage exposes the session to XSS.
security: []
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/SignupRequest' }
responses:
'200':
description: Account created
content:
application/json:
schema: { $ref: '#/components/schemas/AuthResponse' }
'400': { $ref: '#/components/responses/BadRequest' }
'409':
description: Email already registered
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
'415': { $ref: '#/components/responses/UnsupportedMediaType' }
/login:
post:
tags: [Auth]
summary: Authenticate
description: >
Returns 401 for both a wrong password and an unknown email — deliberately
indistinguishable, so do not derive a "no such account" message from it.
Sets the aframp_session HttpOnly cookie on success.
security: []
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/LoginRequest' }
responses:
'200':
description: Authenticated
content:
application/json:
schema: { $ref: '#/components/schemas/AuthResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'415': { $ref: '#/components/responses/UnsupportedMediaType' }
/logout:
post:
tags: [Auth]
summary: Clear the session cookie
description: >
Unauthenticated by design — a browser holding an expired or malformed
session still needs a way to clear it. This ends the browser session
only; it does not revoke a JWT that has been copied elsewhere.
security: []
responses:
'204':
description: Session cookie expired
headers:
Set-Cookie:
schema: { type: string }
description: aframp_session with Max-Age=0
/me:
get:
tags: [Auth]
summary: Current user profile
description: >
The JWT carries only ids, so call this after a page reload to render
human-readable identity without forcing a re-login.
responses:
'200':
description: Profile
content:
application/json:
schema: { $ref: '#/components/schemas/Me' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/wallet/create:
post:
tags: [Wallet]
summary: Generate a Stellar wallet
description: >
Generates a real ed25519 keypair; the private key is AES-256-GCM encrypted
server-side and never returned. NOT idempotent — each call creates another
wallet, and GET /wallet then returns the newest. Check GET /wallet first.
requestBody:
required: false
content:
application/json:
schema: { $ref: '#/components/schemas/CreateWalletRequest' }
responses:
'200':
description: Wallet created
content:
application/json:
schema: { $ref: '#/components/schemas/Wallet' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'415': { $ref: '#/components/responses/UnsupportedMediaType' }
/wallet:
get:
tags: [Wallet]
summary: Get the merchant's most recent wallet
responses:
'200':
description: Wallet
content:
application/json:
schema: { $ref: '#/components/schemas/Wallet' }
'400':
description: No wallet created yet — run onboarding
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
'401': { $ref: '#/components/responses/Unauthorized' }
/payment-requests:
post:
tags: [Payment requests]
summary: Create a payment request
description: >
The core POS action. Returns a SEP-0007 URI to render as a QR code.
sep7_uri is null for non-XLM assets (no cNGN issuer address configured yet).
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CreatePaymentRequest' }
responses:
'200':
description: Payment request created
content:
application/json:
schema: { $ref: '#/components/schemas/PaymentRequest' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'415': { $ref: '#/components/responses/UnsupportedMediaType' }
get:
tags: [Payment requests]
summary: List the merchant's payment requests
description: Newest first, scoped to the authenticated merchant.
parameters:
- $ref: '#/components/parameters/Limit'
responses:
'200':
description: Payment requests
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/PaymentRequest' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
/payment-requests/{id}:
get:
tags: [Payment requests]
summary: Read a payment request (public)
description: >
Deliberately unauthenticated so a customer's device can read a request
before paying. Poll every 3-5s to detect payment; deposit detection runs
on a timer (default 60s), so expect up to ~60s of latency, not instant.
security: []
parameters:
- name: id
in: path
required: true
schema: { type: string, format: uuid }
responses:
'200':
description: Payment request
content:
application/json:
schema: { $ref: '#/components/schemas/PaymentRequest' }
'404': { $ref: '#/components/responses/NotFound' }
/balance:
get:
tags: [Money]
summary: Balances by asset
description: Returns an empty array for a new merchant — not an error.
responses:
'200':
description: Balances
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Balance' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
/transactions:
get:
tags: [Money]
summary: Detected incoming payments
parameters:
- $ref: '#/components/parameters/Limit'
responses:
'200':
description: Payments
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Payment' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
/withdraw:
post:
tags: [Money]
summary: Withdraw to a Nigerian bank account
description: >
Debits the balance and calls Paystack Transfers. Payouts do not currently
complete — Aframp's Paystack balance is unfunded, so live calls return 502.
On failure the balance is automatically refunded and the withdrawal is
recorded with status "failed" plus a failure_reason; nothing is lost.
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CreateWithdrawalRequest' }
responses:
'200':
description: Withdrawal accepted by the provider
content:
application/json:
schema: { $ref: '#/components/schemas/Withdrawal' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'415': { $ref: '#/components/responses/UnsupportedMediaType' }
'502':
description: Payout provider failed; balance already refunded
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
/withdrawals:
get:
tags: [Money]
summary: List withdrawals
parameters:
- $ref: '#/components/parameters/Limit'
responses:
'200':
description: Withdrawals
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Withdrawal' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: >
HS256, 24h expiry. No refresh endpoint — a 401 means re-login. For API
clients and scripts; browsers should use cookieAuth instead.
cookieAuth:
type: apiKey
in: cookie
name: aframp_session
description: >
HttpOnly session cookie set by /signup and /login, cleared by /logout.
The preferred method for browsers — JS cannot read it, so an XSS on the
page cannot steal the session. Never copy the token into localStorage.
parameters:
Limit:
name: limit
in: query
required: false
description: Max rows to return. Clamped server-side to 1-200.
schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
responses:
BadRequest:
description: Validation failed, or the account has no merchant
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
Unauthorized:
description: Missing, malformed, or expired token
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
NotFound:
description: Resource not found
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
UnsupportedMediaType:
description: >
POST/PUT request carries a body but `Content-Type` is not
`application/json` (or a `+json` subtype)
content:
application/json:
schema: { $ref: '#/components/schemas/ApiError' }
schemas:
ApiError:
type: object
required: [error, code]
properties:
error: { type: string, example: insufficient available balance }
code:
type: string
example: INSUFFICIENT_BALANCE
description: Machine-readable error code. Full catalog: INVALID_PARAMETERS, INVALID_AMOUNT, INSUFFICIENT_BALANCE, UNSUPPORTED_ASSET, PAYOUT_FAILED, EMAIL_TAKEN, INVALID_CREDENTIALS, USER_NOT_FOUND, MERCHANT_NOT_FOUND, WALLET_NOT_FOUND, PAYMENT_REQUEST_NOT_FOUND, INTERNAL_ERROR
SignupRequest:
type: object
required: [email, password, name]
properties:
email: { type: string, format: email }
password: { type: string, minLength: 8 }
name: { type: string, minLength: 1 }
LoginRequest:
type: object
required: [email, password]
properties:
email: { type: string, format: email }
password: { type: string }
AuthResponse:
type: object
required: [token, user_id]
properties:
token: { type: string }
user_id: { type: string, format: uuid }
merchant_id:
type: string
format: uuid
nullable: true
description: Null for an account with no merchant; such accounts get 400 from merchant-scoped endpoints.
Me:
type: object
required: [user_id, email, name, created_at]
properties:
user_id: { type: string, format: uuid }
email: { type: string, format: email }
name: { type: string }
created_at: { type: string, format: date-time }
merchant_id: { type: string, format: uuid, nullable: true }
merchant_name: { type: string, nullable: true }
CreateWalletRequest:
type: object
properties:
network: { type: string, default: stellar }
Wallet:
type: object
required: [id, merchant_id, address, network, created_at]
properties:
id: { type: string, format: uuid }
merchant_id: { type: string, format: uuid }
address:
type: string
description: Stellar ed25519 public key (G...).
example: GDDTPSD7BWERBIKVYXJY4KMBVFCUKNGJB2CS3DWBUUO3IB2CV7BZ5WSR
network: { type: string, example: stellar }
created_at: { type: string, format: date-time }
CreatePaymentRequest:
type: object
required: [amount_stroops]
properties:
amount_stroops:
type: integer
format: int64
minimum: 1
description: Amount in stroops (1 unit = 10,000,000 stroops).
example: 25000000
asset: { type: string, default: XLM }
expires_in_secs:
type: integer
format: int64
default: 900
description: Clamped server-side to 60-86400.
PaymentRequest:
type: object
required: [id, merchant_id, address, network, amount_stroops, asset, memo, status, expires_at, created_at]
properties:
id: { type: string, format: uuid }
merchant_id: { type: string, format: uuid }
address: { type: string, description: Stellar destination address to pay. }
network: { type: string, example: stellar }
amount_stroops: { type: integer, format: int64 }
asset: { type: string, example: XLM }
memo:
type: string
description: >
Correlation key. A payment without this memo still credits the balance
but leaves the request pending forever. Included in sep7_uri automatically.
status:
type: string
enum: [pending, paid, expired]
description: >
'expired' is computed at read time from expires_at. A request that
expires and is then paid still flips to 'paid'.
expires_at: { type: string, format: date-time }
created_at: { type: string, format: date-time }
sep7_uri:
type: string
nullable: true
description: >
SEP-0007 payment URI to render as a QR code. Null for non-XLM assets —
no cNGN issuer address is configured, and a guessed one would misdirect funds.
example: web+stellar:pay?destination=GDDT...&amount=2.5000000&memo=1f97c93409172a7d&memo_type=MEMO_TEXT
Balance:
type: object
required: [merchant_id, asset, available, pending, updated_at]
properties:
merchant_id: { type: string, format: uuid }
asset: { type: string, example: XLM }
available: { type: integer, format: int64, description: Withdrawable, in stroops. }
pending:
type: integer
format: int64
description: Detected but unconfirmed. Almost always 0 — no confirmation-depth threshold yet.
updated_at: { type: string, format: date-time }
Payment:
type: object
required: [id, merchant_id, wallet_id, wallet_address, tx_hash, amount_stroops, asset, network, status, confirmations, created_at, updated_at]
properties:
id: { type: string, format: uuid }
merchant_id: { type: string, format: uuid }
wallet_id: { type: string, format: uuid }
wallet_address: { type: string }
tx_hash:
type: string
description: Real Stellar transaction hash; linkable to a block explorer.
amount_stroops: { type: integer, format: int64 }
asset: { type: string }
network: { type: string, example: stellar }
status: { type: string, enum: [detected, verified, confirmed, failed] }
confirmations:
type: integer
description: Currently always 0 — confirmation-depth tracking is not implemented. Do not display.
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
CreateWithdrawalRequest:
type: object
required: [amount_stroops, bank_code, account_number]
properties:
amount_stroops:
type: integer
format: int64
description: >
Must be a whole multiple of 100000 (1 kobo). Paystack's minimum
transfer is NGN 50 = 500000000 stroops.
example: 500000000
asset:
type: string
default: cNGN
description: Only cNGN is accepted.
bank_code: { type: string, example: '058' }
account_number:
type: string
description: Exactly 10 digits (NUBAN).
example: '0123456789'
Withdrawal:
type: object
required: [id, merchant_id, amount_stroops, asset, status, created_at, updated_at]
properties:
id: { type: string, format: uuid }
merchant_id: { type: string, format: uuid }
amount_stroops: { type: integer, format: int64 }
asset: { type: string, example: cNGN }
status: { type: string, enum: [pending, processing, completed, failed] }
provider: { type: string, nullable: true, example: paystack }
provider_reference: { type: string, nullable: true }
bank_code: { type: string, nullable: true }
account_number: { type: string, nullable: true }
failure_reason:
type: string
nullable: true
description: Provider's own wording on a failed payout. Show this to the merchant.
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }