Payment Intents
A payment intent represents a customer’s intention to pay a specific amount to a merchant. It’s the core object in the Stable Genius API — every payment starts with creating one.
Lifecycle
A payment intent moves through the following statuses:
| Status | Description |
|---|
awaiting_payment | Payment intent created. QR code displayed. Waiting for customer to send USDC. |
confirmed | USDC received and confirmed on-chain. Webhook fired. Fees split. |
expired | No payment received within the TTL window (default: 5 minutes). |
cancelled | Integrator cancelled the payment intent before payment was received. |
settled | Funds have been withdrawn from the merchant’s on-chain wallet and sent to their bank. |
Creating a Payment Intent
Required Fields
| Field | Type | Description |
|---|
amount | number | Payment amount in USD. Minimum: 0.01.Maximum:10,000. |
currency | string | Always "usd" for now. |
merchant_id | string | The merchant’s Stable Genius ID (format: mer_*). |
Optional Fields
| Field | Type | Description |
|---|
metadata | object | Arbitrary key-value pairs attached to the payment intent. Returned in webhooks. Use for order IDs, terminal IDs, customer references, etc. Max 10 keys, 500 chars per value. |
ttl | integer | Time-to-live in seconds before the intent expires. Default: 300 (5 minutes). Min: 60. Max: 3600. |
idempotency_key | string | Unique key to prevent duplicate payment intents. If you send the same key within 24 hours, we return the existing intent instead of creating a new one. |
Response Fields
| Field | Type | Description |
|---|
id | string | Unique payment intent ID (format: pi_*). |
status | string | Current status (see lifecycle above). |
amount | number | Payment amount in USD. |
payment_address | string | The on-chain address the customer should send USDC to. |
chain | string | Blockchain network (base). |
token | string | Token symbol (USDC). |
qr_payload | string | EIP-681 formatted URI for QR code rendering. Wallet-compatible. |
qr_image_url | string | Hosted PNG image of the QR code. 400x400px. |
expires_at | string | ISO 8601 timestamp when the intent expires. |
metadata | object | Your attached metadata, returned as-is. |
created_at | string | ISO 8601 timestamp. |
Idempotency
Use the idempotency_key field to safely retry requests. If a network error occurs and you’re unsure whether the payment intent was created, resend the same request with the same idempotency key. We’ll return the existing intent if it was already created, or create a new one if it wasn’t.
curl -X POST https://api.stablegenius.co/v1/payment-intents \
-H "Authorization: Bearer sk_test_abc123" \
-H "Content-Type: application/json" \
-d '{
"amount": 4.50,
"currency": "usd",
"merchant_id": "mer_abc123",
"idempotency_key": "order_456_attempt_1"
}'
Idempotency keys expire after 24 hours.
Expiration
Payment intents expire after the TTL window (default: 5 minutes). If no USDC is received by expires_at, the status transitions to expired and the payment address is released.
If a customer sends USDC to an expired payment intent’s address, the funds are still received by the merchant’s proxy contract and will be recorded as a transaction — but no payment_intent.confirmed webhook will fire for that specific intent. Use the transaction.created webhook to catch these edge cases.
Attach up to 10 key-value pairs to a payment intent. Metadata is not used by Stable Genius — it’s passed through to webhooks so your system can correlate payments with orders, terminals, or customers.
Common metadata patterns:
{
"metadata": {
"order_id": "order_456",
"terminal_id": "pos_01",
"customer_ref": "cust_789",
"location": "sf_ferry_building"
}
}