RMO Developers

Payment Codes

A member generates a 10-character code + 7-character PIN; a merchant redeems it against an amount. Lockout-protected, single-use by default, can be merchant-locked or recurring.

When to use payment codes. Payment codes work without a card, terminal, or wallet — useful for over-the-phone orders, P2P pickups, vending, kiosks, or in-store sales when a member doesn’t have their card on them. Every redemption writes an Issuing.Authorizations row, so codes land in the same ledger as card authorizations.

Lifecycle

POST /v1/payment-codes
  ↓
[Code Active]                  — 10-min expiry by default
  ↓  POST /authorize  (correct PIN)
[Code Used]                    — single-use codes
  + Issuing.Authorizations row written

  ↓  POST /authorize  (wrong PIN, < lockoutThreshold)
[Code Active] but Attempts++
  ↓  POST /authorize  (wrong PIN, >= lockoutThreshold)
[Code Locked]                  — cannot be redeemed without revoke + reissue

  ↓  POST /<code>/revoke
[Code Revoked]                 — terminal

Create a code

POST/v1/payment-codes

Mint a payment code on behalf of a member. The response returns code + pin — the PIN appears only once. Surface both to the member immediately; we don’t store the plaintext.

FieldTypeDescription
member requiredstringRelationship.Accounts.RecordId — whose account funds the redemption.
account optionalstringSpecific Financial.Accounts.RecordId to debit. Defaults to the member’s primary.
merchant optionalstringLock to one Merchant.Accounts.RecordId. Other merchants get a decline.
maxAmount optionalnumberPer-redemption cap. Redemptions over this are declined.
currency optionalstringCore.Currencies.RecordId. USD default.
expiryMinutes optionalintegerDefault 10.
singleUse optionalbooleanDefault true. Mutually exclusive with allowRecurring.
allowRecurring optionalbooleanDefault false. When true, set recurringInterval + recurringLimit.
recurringInterval optionalenumdaily / weekly / monthly
recurringLimit optionalintegerMax successful uses across the recurring lifetime.
recurringThru optionaldateEnd date for recurring codes.
lockoutThreshold optionalintegerBad-PIN attempts before the code is locked. Default 5.
displayHint optionalstringUI label the member sees alongside the code.
metadata optionalobjectJSON passthrough.
curl https://api.rmous.org/v1/payment-codes \
  -H "X-API-Key: pk_test_publicsandbox_2026" \
  -H "Authorization: Bearer sk_test_publicsandbox_2026_anyone_can_use_this" \
  -H "Idempotency-Key: mint-2026-05-14-001" \
  -H "Content-Type: application/json" \
  -d '{
    "member":     "MEMBER_RECORD_ID",
    "maxAmount":  50.00,
    "merchant":   "MERCHANT_RECORD_ID",
    "singleUse":  true,
    "expiryMinutes": 15,
    "displayHint":"For tonight's pickup at RMO Coffee"
  }'

# Response (201) - PIN returned ONCE:
{
  "recordId":  "PcRRdY1mQXz3VnaFx",
  "code":      "A7BX3FQM2N",         // 10 chars (Crockford)
  "pin":       "9KQVZD2",            // 7 chars - returned ONCE
  "expiresAt": "2026-05-14T17:08:11Z",
  "status":    "Code Active"
}
The PIN is shown only once. If your integration loses the PIN before surfacing it, you must revoke the code and mint a new one. Idempotent retries return the prior code (without the PIN).

Alphabet

Codes and PINs are drawn from the Crockford base-32 alphabet (0-9, A-Z without I, L, O, U). Safe to dictate over the phone, hard to misread.

Redeem (merchant-side)

POST/v1/payment-codes/authorize

Merchant submits code + pin + amount. On success, an Issuing.Authorizations row is written and the code transitions to Code Used (if single-use). On wrong PIN, Attempts increments and the code locks once lockoutThreshold is reached.

FieldTypeDescription
code requiredstring(10)The code the member shows.
pin requiredstring(7)The PIN the member provides verbally / on a keypad.
amount requirednumberPositive number.
merchant requiredstringMerchant.Accounts.RecordId. Must match the code’s lock if one is set.
currency optionalstringUSD default.
deviceFingerprint optionalstringAudit signal — helps with fraud analysis.
metadata optionalobjectJSON passthrough.
curl https://api.rmous.org/v1/payment-codes/authorize \
  -H "X-API-Key: $RMO_API_KEY" \
  -H "Authorization: Bearer $RMO_BEARER" \
  -H "Idempotency-Key: redeem-A7BX3FQM2N-1" \
  -H "Content-Type: application/json" \
  -d '{
    "code":     "A7BX3FQM2N",
    "pin":      "9KQVZD2",
    "amount":   42.50,
    "merchant": "MERCHANT_RECORD_ID"
  }'

# Approved (201):
{
  "status":          "approved",
  "authorizationId": "7421594",
  "recordId":        "R1d2C3o4D5e6P7i8N",
  "amount":          42.50,
  "member":          "MEMBER_RECORD_ID",
  "expires":         "2026-05-21T17:08:11Z"
}

# Declined (200) - wrong PIN, lockout countdown:
{
  "status":        "declined",
  "reasonCode":    "75",
  "reasonMessage": "Invalid PIN",
  "amount":        42.50
}

Other declines: 54 code expired, 61 amount over maxAmount, 62 code revoked / locked to a different merchant, 75 code locked.

Read

GET/v1/payment-codes/{code}

Returns the code’s current state. The PIN is never returned, regardless of which tenant asks.

curl https://api.rmous.org/v1/payment-codes/A7BX3FQM2N \
  -H "X-API-Key: $RMO_API_KEY" \
  -H "Authorization: Bearer $RMO_BEARER"

Revoke

POST/v1/payment-codes/{code}/revoke

Permanently revoke a code. Useful when a member loses access to their PIN, or you suspect compromise. Already-redeemed codes can’t be un-redeemed — this only prevents future use.

curl -X POST https://api.rmous.org/v1/payment-codes/A7BX3FQM2N/revoke \
  -H "X-API-Key: $RMO_API_KEY" \
  -H "Authorization: Bearer $RMO_BEARER" \
  -H "Content-Type: application/json" \
  -d '{"reason": "member requested cancellation"}'

# Response:
{ "revoked": true, "recordId": "PcRRdY1mQXz3VnaFx" }

Where to next

Listen for events → Error reference → Card authorizations →