Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: IBANforge Account API
version: 1.9.0
description: IBANforge checks the bank behind an IBAN before you pay.
contact:
name: IBANforge support
url: https://github.com/cammac-creator/ibanforge/issues
email: support@ibanforge.com
servers:
- url: https://api.ibanforge.com
description: Production
- url: http://localhost:3000
description: Local development
tags:
- name: Account
description: 'The account page, https://ibanforge.com/account, for a person in a browser: a 6-digit code mailed to the address of the keys, then a read-only session cookie that shows every key of that address. Rotating or revoking a key still takes the key itself.'
paths:
/v1/account/code:
post:
operationId: requestAccountSignInCode
summary: Mail a 6-digit sign-in code for the account page
description: 'First step of signing in to the account page, https://ibanforge.com/account. Made for a person in a browser: the address receives a 6-digit code, and POST /v1/account/session exchanges it for a read-only session. The code is valid 15 minutes and allows 5 tries; a new code replaces the previous one. The same 202 answers, and the same mail leaves, whether or not the address carries keys: this route never tells whether an address holds a key. The code is mailed to the normalized form of the address: a "+tag" is dropped, and at Gmail the dots too. The codes mailed to one address, one domain and one network are capped per day, in one budget shared with POST /v1/keys/generate and POST /v1/keys/claim. Send the request as application/json; a browser Origin that is not the site is refused. An agent holding a key reads the same figures with GET /v1/keys/usage and GET /v1/keys/report, and has no reason to call this route. Never send an address your human has not handed you for this purpose.'
tags:
- Account
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- email
properties:
email:
type: string
format: email
maxLength: 254
example: you@example.com
description: 'One plain address: no list, no display name, no quotes.'
responses:
'202':
description: A code left for this address. The same body answers for every address.
content:
application/json:
schema:
type: object
required:
- status
- expires_in
properties:
status:
type: string
enum:
- code_sent
expires_in:
type: integer
example: 900
description: Seconds the code stays valid.
'400':
description: '"invalid_json": the body is not a JSON object. "invalid_email": not one plain address, or its normalized form is not one. "disposable_email": a throwaway or placeholder domain. "undeliverable_email": the domain has no mail server, or the mail server refused the address.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'401':
description: '"signed_out": the request carried the account cookie twice. The cookie is cleared.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: '"forbidden_origin": the browser Origin is not allowed.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'413':
description: Request body exceeds 256 KB. Applied globally to every operation that takes a body, before routing and before payment, so nothing is charged. Split the input (batch validation accepts up to 100 IBANs per call).
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'415':
description: '"unsupported_media_type": send the request as application/json.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'429':
description: '"code_rate_limited": too many codes today for this address, its domain or this network. Try again tomorrow, or paste an API key on the account page.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'503':
description: '"code_unavailable": sign-in codes cannot be sent right now (the mail relay is down, or the hourly ceiling of sign-in codes is reached). Try again later, or paste an API key on the account page.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
/v1/account/session:
post:
operationId: openAccountSession
summary: Exchange the sign-in code for a session cookie
description: 'Second step of signing in to the account page. A right code opens a session: the answer sets the cookie ibanforge_account (HttpOnly, Secure, SameSite=Strict, Path=/v1/account, 7 days from sign-in) and never carries the session token in its body. Every code that cannot be used (wrong, expired, tried too many times, never asked for, or not six digits) gets the same 400 "invalid_code": ask for a new code. An entry that is not six digits does not count as a try. A right code opens a session whether or not the address carries keys; GET /v1/account/overview then says what it holds. The session reads and never writes: it cannot rotate, revoke, claim or top up a key, and it opens no paid route. Same write rules as POST /v1/account/code: application/json, and the browser Origin is checked.'
tags:
- Account
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- email
- code
properties:
email:
type: string
format: email
maxLength: 254
example: you@example.com
description: The address the code was asked for, written as the person typed it.
code:
type: string
pattern: ^[0-9]{6}$
example: '123456'
description: The 6-digit code of the most recent mail.
responses:
'200':
description: Signed in. Set-Cookie carries the session; the body only says so, with the end of the session.
headers:
Set-Cookie:
description: ibanforge_account=…; Max-Age=604800; Path=/v1/account; HttpOnly; Secure; SameSite=Strict
schema:
type: string
content:
application/json:
schema:
type: object
required:
- signed_in
- expires_at
properties:
signed_in:
type: boolean
enum:
- true
expires_at:
type: string
format: date-time
'400':
description: '"invalid_json", "invalid_email", or "invalid_code": one answer for every code that cannot be used. Ask for a new code with POST /v1/account/code.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'401':
description: '"signed_out": the request carried the account cookie twice. The cookie is cleared.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: '"forbidden_origin": the browser Origin is not allowed.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'413':
description: Request body exceeds 256 KB. Applied globally to every operation that takes a body, before routing and before payment, so nothing is charged. Split the input (batch validation accepts up to 100 IBANs per call).
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'415':
description: '"unsupported_media_type": send the request as application/json.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'429':
description: Rate limit exceeded. Applied globally by the server, so any operation can answer it. Honour the Retry-After header; see https://api.ibanforge.com/rate-limits.yml
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
/v1/account/overview:
get:
operationId: getAccountOverview
summary: Every active key of the signed-in address
description: 'Read-only view of the account page: the active keys whose address normalizes to the signed-in one, 50 per page, the most recently called first. For each key: its prefix (never the key itself), its plan, its monthly allowance (the figures of GET /v1/keys/usage) or its credit balance, the calls of this month, the last call, the alerts mailed, and the link that manages a Pro or Editor subscription. `inactive_keys` counts the deactivated keys of the address, without detail. Authentication is the session cookie set by POST /v1/account/session; a browser sends it with credentials: "include". Never cached (Cache-Control: no-store).'
tags:
- Account
security:
- accountSession: []
parameters:
- name: page
in: query
required: false
description: Page number, from 1.
schema:
type: integer
minimum: 1
default: 1
responses:
'200':
description: The overview of the signed-in address.
content:
application/json:
schema:
$ref: '#/components/schemas/AccountOverview'
'401':
description: '"signed_out": no session, an expired or revoked one, or the account cookie sent twice. A cookie that leads to no live session is cleared.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'429':
description: Rate limit exceeded. Applied globally by the server, so any operation can answer it. Honour the Retry-After header; see https://api.ibanforge.com/rate-limits.yml
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
/v1/account/keys/report:
get:
operationId: getAccountKeyReport
summary: The report of one key of the signed-in address
description: 'The same body as GET /v1/keys/report (key_prefix, usage, report), for one key of the signed-in address named by its prefix, without the key itself. The prefix travels as a query parameter, never in the path. The window is capped at 90 days here, and report.window_days says the window served. A prefix that is unknown, deactivated or attached to another address gets the same 404. Never cached (Cache-Control: no-store).'
tags:
- Account
security:
- accountSession: []
parameters:
- name: prefix
in: query
required: true
description: The key_prefix of the key, as the overview lists it.
schema:
type: string
maxLength: 64
example: ifk_3f9c1a7e
- name: days
in: query
required: false
description: Window in days, clamped to 1..90. Defaults to 30.
schema:
type: integer
minimum: 1
maximum: 90
default: 30
responses:
'200':
description: key_prefix, usage (as GET /v1/keys/usage serves it) and report (as GET /v1/keys/report serves it).
'401':
description: '"signed_out": no live session, or the account cookie sent twice.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: '"not_found": no such key in this account. The same answer for an unknown prefix and for the prefix of another address.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'429':
description: Rate limit exceeded. Applied globally by the server, so any operation can answer it. Honour the Retry-After header; see https://api.ibanforge.com/rate-limits.yml
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
/v1/account/receipts:
get:
operationId: getAccountReceipts
summary: The purchases paid by the signed-in address, to find their receipts
description: 'The credit packs and subscriptions paid by the signed-in address, the most recent first, 100 at most. A card payment belongs to the address its payer gave at checkout, and to that address alone; a USDC payment, and a card payment recorded before the purchase register existed, belong to the address of the key they landed on (a rotated or deactivated key included). Only money that was actually taken: nothing pending or failed; a refunded or disputed purchase stays, with its status. Each purchase is named by an opaque `ref`, never by a sequential number. For a pack paid by card, `receipt` is the path of GET /v1/account/receipt that gives its Stripe receipt; for a card subscription, `invoices` is the Stripe customer portal where its invoices live; a USDC purchase has neither. No call to Stripe is made here, and no receipt link is served in this list. Authentication is the session cookie. Never cached (Cache-Control: no-store).'
tags:
- Account
security:
- accountSession: []
responses:
'200':
description: The purchases of the signed-in address.
content:
application/json:
schema:
$ref: '#/components/schemas/AccountReceipts'
'401':
description: '"signed_out": no session, an expired or revoked one, or the account cookie sent twice. A cookie that leads to no live session is cleared.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'429':
description: Rate limit exceeded. Applied globally by the server, so any operation can answer it. Honour the Retry-After header; see https://api.ibanforge.com/rate-limits.yml
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
/v1/account/receipt:
get:
operationId: getAccountReceiptLink
summary: The Stripe receipt of one purchase of the signed-in address
description: 'Asks Stripe for the receipt of one pack paid by card, named by the `ref` GET /v1/account/receipts lists, and answers its link (`url`, which always starts with https://pay.stripe.com/receipts/): the receipt opens on Stripe. Stripe makes a receipt link expire 30 days after giving it, never the receipt itself: open the link, do not store it; ask this route again for a fresh one. The link opens the receipt for whoever holds it. The ref travels as a query parameter. A ref that is unknown, belongs to another address or names a purchase without a card receipt (a subscription, a USDC payment) gets the same 404. Never cached (Cache-Control: no-store).'
tags:
- Account
security:
- accountSession: []
parameters:
- name: ref
in: query
required: true
description: The ref of the purchase, as GET /v1/account/receipts lists it.
schema:
type: string
pattern: ^rcpt_[0-9a-f]{24}$
example: rcpt_3f9c1a7e5b2d4c6e8a0b1c2d
responses:
'200':
description: The link of the Stripe receipt.
content:
application/json:
schema:
type: object
required:
- ref
- url
properties:
ref:
type: string
url:
type: string
format: uri
description: The Stripe receipt, valid 30 days. Always starts with https://pay.stripe.com/receipts/
'401':
description: '"signed_out": no live session, or the account cookie sent twice.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: '"receipt_not_found": no such receipt in this account. The same answer for an unknown ref, the ref of another address, and a purchase without a card receipt.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'429':
description: Rate limit exceeded. Applied globally by the server, so any operation can answer it. Honour the Retry-After header; see https://api.ibanforge.com/rate-limits.yml
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'503':
description: '"receipt_unavailable": Stripe did not give the receipt just now. Try again in a minute; support@ibanforge.com sends it otherwise.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
/v1/account/logout:
post:
operationId: closeAccountSession
summary: Sign out of the account page, here or everywhere
description: 'Ends the session of this browser and clears its cookie. With {"all": true}, ends every session of the signed-in address (sign out everywhere). Signing out with no live session is not an error: 204 all the same. Same write rules as POST /v1/account/code: application/json, and the browser Origin is checked.'
tags:
- Account
security:
- accountSession: []
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
all:
type: boolean
default: false
description: true ends every session of the address, in every browser.
responses:
'204':
description: Signed out. The cookie is cleared.
'400':
description: '"invalid_json": the body is present and is not a JSON object.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'401':
description: '"signed_out": the request carried the account cookie twice. The cookie is cleared and nothing is revoked.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: '"forbidden_origin": the browser Origin is not allowed.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'413':
description: Request body exceeds 256 KB. Applied globally to every operation that takes a body, before routing and before payment, so nothing is charged. Split the input (batch validation accepts up to 100 IBANs per call).
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'415':
description: '"unsupported_media_type": send the request as application/json.'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'429':
description: Rate limit exceeded. Applied globally by the server, so any operation can answer it. Honour the Retry-After header; see https://api.ibanforge.com/rate-limits.yml
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
components:
schemas:
AccountOverview:
type: object
required:
- email
- session_expires_at
- month
- page
- pages
- keys
- inactive_keys
properties:
email:
type: string
description: The address typed at sign-in, in lower case.
example: you@example.com
session_expires_at:
type: string
format: date-time
description: When the session ends; sign in again after it.
month:
type: string
example: 2026-09
description: The calendar month (UTC) that calls_this_month counts.
page:
type: integer
minimum: 1
pages:
type: integer
minimum: 1
keys:
type: array
items:
$ref: '#/components/schemas/AccountKey'
inactive_keys:
type: integer
description: Deactivated keys of the address (revoked or rotated), counted without detail.
AccountReceipt:
type: object
required:
- ref
- paid_at
- kind
- plan
- rail
- credits
- amount
- status
- key_prefix
- receipt
- invoices
properties:
ref:
type: string
example: rcpt_3f9c1a7e5b2d4c6e8a0b1c2d
description: The opaque reference of the purchase, for GET /v1/account/receipt.
paid_at:
type:
- string
- 'null'
format: date-time
description: When the payment settled, else when it was recorded (UTC).
kind:
type: string
enum:
- pack
- subscription
plan:
type:
- string
- 'null'
enum:
- pro
- editor
- null
description: The plan of a subscription; null for a pack, or when unknown.
rail:
type: string
enum:
- card
- usdc
credits:
type:
- integer
- 'null'
description: The credits of a pack; null for a subscription.
amount:
type:
- object
- 'null'
description: The amount taken, in minor units of its currency (400 and "usd" is 4.00 USD); null when unknown.
required:
- minor
- currency
properties:
minor:
type: integer
currency:
type: string
example: usd
status:
type: string
enum:
- paid
- refunded
- disputed
key_prefix:
type: string
example: ifk_3f9c1a7e
description: The key the purchase landed on. The key itself is never served.
receipt:
type:
- string
- 'null'
example: /v1/account/receipt?ref=rcpt_3f9c1a7e5b2d4c6e8a0b1c2d
description: 'For a pack paid by card: the path, on the API host, that gives its Stripe receipt. Null otherwise.'
invoices:
type:
- string
- 'null'
format: uri
description: 'For a subscription paid by card: the Stripe customer portal, where its invoices live. Null otherwise.'
AccountReceipts:
type: object
required:
- receipts
properties:
receipts:
type: array
items:
$ref: '#/components/schemas/AccountReceipt'
AccountKey:
type: object
required:
- key_prefix
- created_at
- plan
- allowance
- credits
- subscription
- calls_this_month
- last_call_at
- alerts
- address_proven
- actions
properties:
key_prefix:
type: string
example: ifk_3f9c1a7e
description: The prefix of the key. The key itself is never served.
created_at:
type:
- string
- 'null'
format: date-time
plan:
type: string
enum:
- free
- custom
- pack
- none
- pro
- editor
- free+pack
- custom+pack
- pro+pack
- editor+pack
description: 'A key that holds an allowance AND prepaid credits carries both parts, such as free+pack: the allowance is drawn first, then the credits.'
allowance:
type:
- object
- 'null'
description: The allowance, with the figures of GET /v1/keys/usage. null on a key born of a purchase, which has no allowance of its own and whose balance is in credits.
properties:
basis:
type: string
enum:
- monthly
- lifetime
limit:
type: integer
used:
type: integer
remaining:
type: integer
credits:
type:
- object
- 'null'
description: The prepaid balance of a key that holds credits, alone or beside an allowance. purchased_total is the total ever bought on the key, recharges included. null on a key without credits.
properties:
remaining:
type: integer
purchased_total:
type: integer
subscription:
type:
- object
- 'null'
properties:
plan:
type: string
enum:
- pro
- editor
status:
type: string
enum:
- active
manage_url:
type: string
format: uri
description: 'The customer portal: card, invoices, cancellation.'
calls_this_month:
type: integer
description: Calls billed to the key this month, credit calls included.
last_call_at:
type:
- string
- 'null'
format: date-time
alerts:
type: array
description: The alerts mailed for this key, the most recent first.
items:
type: object
properties:
kind:
type: string
enum:
- quota_80
- credits_low
sent_at:
type:
- string
- 'null'
format: date-time
address_proven:
type: boolean
description: 'True when the address of this key was proven by a code (created or claimed with a 6-digit code). An address typed at a checkout, or given to a first key without a code, is not: the page then asks you to recognise the key before recharging it.'
actions:
type: object
description: Links the page may offer. topup recharges THIS key by card (the links carry its recharge reference, never the key); subscribe_pro is the Pro link that puts the subscription on THIS key, null on a key that already carries one; manage_subscription is the portal of a subscribed key.
properties:
topup:
type:
- object
- 'null'
properties:
1k:
type: string
format: uri
5k:
type: string
format: uri
25k:
type: string
format: uri
subscribe_pro:
type:
- string
- 'null'
manage_subscription:
type:
- string
- 'null'
ApiError:
type: object
required:
- error
- message
additionalProperties: true
properties:
error:
type: string
description: 'Stable machine-readable token in snake_case, e.g. "invalid_json", "invalid_request", "batch_too_large", "payment_required", "payload_too_large", "rate_limit_exceeded". Branch on this, never on `message`. An invalid IBAN is not an ApiError: validation answers 200 with `valid: false`.'
example: batch_too_large
message:
type: string
description: Human-readable sentence explaining the failure. Wording may change; the token above will not.
example: Maximum 100 IBANs per batch request
securitySchemes:
x402Payment:
type: apiKey
in: header
name: PAYMENT-SIGNATURE
description: x402 USDC micropayment signature (protocol v2). Clients holding v1 payment requirements may send the same signature as X-Payment; both are accepted.
apiKey:
type: http
scheme: bearer
description: API key (Bearer ifk_xxx) — 25 free requests/month without an email address, 200 a month once claimed, or a custom quota for paid keys
accountSession:
type: apiKey
in: cookie
name: ibanforge_account
description: 'Session of the account page, set by POST /v1/account/session: HttpOnly, Secure, SameSite=Strict, Path=/v1/account, 7 days from sign-in. Read-only: it opens no paid route and no route that acts on a key.'
externalDocs:
description: Agent-oriented overview (llms.txt) with copy-paste examples
url: https://api.ibanforge.com/llms.txt