Payments

The value-transfer primitive. Send instant payments via the GraphQL gateway, poll the REST message-status endpoint until settled, query the unified activity feed.

The value-transfer primitive. A payment is what an activated deal — or a one-off instant mutation — executes when value needs to move between parties. The payment itself is idempotency-keyed, on-chain, and confidential.

In production flows you rarely create a raw payment by hand — the DMS does it when a signed agreement compiles down to executable actions. The mutations below are exposed for the cases where you need them directly: one-off transfers, test flows, or applications that don't need the deal envelope around every movement.

One GraphQL gateway, one JWT. Post payment mutations and joined reads to $YF_GATEWAY/graphql (api.yieldfabric.com/graphql). Status polling for async on-chain work uses $YF_PAYMENTS/api/users/.../messages/... over REST — that one's not GraphQL.

Send an instant payment

Transfer value to another party. The payment locks in the sender's account until the recipient accepts.

curl -X POST $YF_GATEWAY/graphql \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation($input: InstantSendInput!) { instant(input: $input) { success messageId paymentId } }",
    "variables": { "input": {
      "destinationWalletId": "550e8400-e29b-41d4-a716-446655440000",
      "assetId": "aud-token-asset",
      "amount": "10",
      "idempotencyKey": "<fresh UUIDv4>"
    }}
  }'

Input shape

Field
destinationWalletId
Required
one of
Notes
The recipient's wallet UUID. Canonical when you have it (/protected/jwt returns default_wallet_id for the bearer).
Field
destinationId
Required
one of
Notes
The recipient's entity name (email, display name). Resolved via entitiesByName. ✗ never pass a raw 0x… here — that's an address, not a name.
Field
assetId
Required
yes
Notes
The asset identifier (e.g. aud-token-asset).
Field
amount
Required
yes
Notes
Integer string"10", not 10 and not "10.00". No decimal points.
Field
idempotencyKey
Required
recommended
Notes
Fresh UUIDv4 per submission. Omit to let the resolver auto-generate. Never deterministic — see pitfall below.
Field
walletId
Required
no
Notes
The sending wallet UUID (defaults to the bearer's default_wallet_id).
Field
requireManualSignature
Required
no
Notes
When true, the message stalls in ManualSignature state until the user signs from their wallet UI. Auto-true if the bearer's is_external_signer flag is set.
Field
contractId
Required
no
Notes
Link this payment to an existing obligation for cashflow reconciliation.

Idempotency rules

Same key + same body
Same response (no double-charge).
Same key + different body
400 IDEMPOTENCY_CONFLICT.

Pitfall — never deterministic. If you compute idempotencyKey as sha256(user_id || action), every retry after a stuck message returns the stuck messageId again without re-enqueueing — the executor never gets a fresh message to run. Either omit the field or use a UUIDv4 per call.

Watch the payment settle

The mutation returns as soon as the GraphQL gateway accepts the message; actual on-chain settlement is asynchronous. The canonical pattern for backend callers (Python SDK, shell scripts, operator) is REST polling at 2-second intervals against the message-status endpoint.

USER_ID=$(curl -s $YF_AUTH/protected/jwt -H "Authorization: Bearer $TOKEN" | jq -r .user_id)

until curl -s -H "Authorization: Bearer $TOKEN" \
  "$YF_PAYMENTS/api/users/$USER_ID/messages/$MESSAGE_ID" \
  | jq -e '.executed' > /dev/null; do
  sleep 2
done

curl -s -H "Authorization: Bearer $TOKEN" \
  "$YF_PAYMENTS/api/users/$USER_ID/messages/$MESSAGE_ID" | jq

Status progression: PendingValidatingExecutingCompleted (or Failed / ManualSignature). Terminal when executed is non-null.

Browser clients can subscribe to the SSE stream (/api/events/stream) or WebSocket (/ws/messages) for live UX instead. Backend services use polling — simpler, no missed-event race, no streaming state to manage.

Query a wallet's activity (history feed)

The modern read pattern. Unified, paginated, merges payments + messages, newest first. Stateless — same (walletId, limit, cursor) returns the same page on any replica.

curl -X POST $YF_GATEWAY/graphql \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "query Activity($walletId: String!, $limit: Int, $cursor: String) {
      walletFlow {
        activity(walletId: $walletId, limit: $limit, cursor: $cursor) {
          nextCursor
          items {
            __typename
            ... on PaymentActivity { timestamp payment { id status amount assetId } }
            ... on MessageActivity { timestamp message { id messageType status executed } }
          }
        }
      }
    }",
    "variables": { "walletId": "'$WALLET_ID'", "limit": 25 }
  }'

Pagination contract:

  • cursor is opaque. Don't parse it. Pass nextCursor from the previous page back as the next cursor.
  • nextCursor: null means the feed is exhausted.
  • limit defaults to 25, clamped to 200.

Sorted timestamp DESC across both streams; ties break by id lexicographically — stable but arbitrary across __typename.

Query a single wallet's payments

curl -X POST $YF_GATEWAY/graphql \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "query GetPayments($currentEntityId: ID) { paymentsByEntity(currentEntityId: $currentEntityId) { id amount assetId paymentType status dueDate description } }",
    "variables": { "currentEntityId": "your-entity-id" }
  }'

Payment lifecycle

Stage
Create
What happens
Sender submits the instant mutation. A message is queued and messageId is returned immediately.
Stage
Validate
What happens
The validator checks balances, builds the unsigned transaction, runs ZKP.
Stage
Sign
What happens
Auto-sign (custodial keystore) or stall in ManualSignature for external-wallet users.
Stage
Execute
What happens
Sender's account locks the amount on-chain; recipient sees it in locked_in.
Stage
Accept
What happens
Recipient's vault picks up the locked amount during their next payment / withdrawal flow. (For most consumer flows you don't have to call accept explicitly — the next outflow reconciles automatically.)
Stage
Complete
What happens
executed field on the message becomes non-null; bitemporal audit row written.

Common pitfalls

Pitfall
GraphQL says success: true but on-chain never settles
Fix
Poll the message-status endpoint. The mutation only confirms that the operation was queued; settlement is async.
Pitfall
"No entity found with name '0x…'"
Fix
You passed an address into a name-shaped field. Use destinationWalletId with a UUID.
Pitfall
403 Insufficient permissions: CryptoOperations
Fix
The sender's role lacks the permission. Auto-granted for SuperAdmin / Admin / Manager / Operator; explicit for Viewer / ApiClient. See Authentication.
Pitfall
Stale balance shown to the user after Completed
Fix
The consumer pipeline takes a few seconds to project the payment into the wallet's positions. Re-query payments after a 2-3s delay.
Pitfall
Same idempotencyKey retried with different body
Fix
Returns 400 IDEMPOTENCY_CONFLICT. Generate a fresh UUIDv4 per logical submission.
Pitfall
Posting to /api/mq/submit from app code
Fix
That's service-to-service only and needs a vault-equipped caller (403 Insufficient permissions: CryptoOperations otherwise). Use the corresponding GraphQL gateway mutation instead.

See also

YieldFabric docs(317)