Skip to main content

Customer-signed accepts

Optional EIP-712 accept: the end-customer’s wallet authorizes each accept. Default remains empty-body {} accept.

Mental model

Two different signatures, in order:
  1. Register once — prove ownership of an EVM wallet for a vault customer (pcus_…). This creates an active signing key (skey_…).
  2. Sign every accept — after quote, the customer signs Element Pay’s quote typed_data; you submit that signature on accept.
Register signature ≠ accept signature. Reusing either in the wrong step will fail.

1. When to use

  • Customer must approve accept in their wallet → enable Signed accepts for that API key.
  • You accept on the customer’s behalf → leave it off; empty {} accept still works.

2. Enable in Console

Enablement is per API key in the Console Settings column (sandbox key ≠ live key). It is not toggled with X-API-Key. Rules:
  • Default off — custodial partners leave it off; quote/accept unchanged.
  • Tooltip on Signed accepts: “Requires customer EIP-712 signature on accept.”
  • Turn it on in Dev Console API Keys (sandbox) or the live console equivalent. Element Pay can also flip the flag ops-side if needed.
With Signed accepts on, empty-body accept is rejected. Desk / accept-on-behalf also fails for that key.

Auth and environment

3. Register a wallet (once per customer)

Prerequisite: vault customer approved (pcus_…) — Customer KYC. Enrollment is API-only (no Console signing-keys UI). EVM addresses only (0x + 40 hex) — not Stellar G….
1

Start challenge

POST /partner/customers/{customer_id}/signing-keys/challenge with { "address": "0x…" }
2

Customer signs

Present our typed_data to the wallet (eth_signTypedData_v4) — do not rebuild it client-side
3

Complete enrollment

POST …/signing-keys/complete with { "key_id", "signature" } → key becomes active
4

Optional manage

List keys, revoke, or re-challenge with a new address to replace a lost wallet

Step A — Start challenge

Challenge success — what to expect

EIP-712 domain differs sandbox vs live. Always use the typed_data from the environment you are calling. Do not forge or rebuild it.

Step B — Customer signs in wallet

Customer signs the challenge typed_data with the same address (MetaMask, WalletConnect, or equivalent → eth_signTypedData_v4).

Step C — Complete enrollment

Complete success — what to expect

That wallet may now sign accepts for this customer.

Step D — Optional manage

Lost wallet: start a new challenge with the new address (re-challenge replaces the active enrollment path). Or revoke, then challenge again.

4. Every order (after a wallet is registered)

Only when Signed accepts is on for the API key and the customer has an active signing key.
1

Quote

POST /partner/orders/quote with customer_id + normal quote fields
2

Customer signs quote typed_data

Use data.signing.typed_data from the quote response (not the register typed_data)
3

Accept with signature

POST /partner/orders/{quote_id}/accept with signed_accept.signature
4

Settle

Deposit / settlement continues as today — webhooks unchanged

Quote

Same as Quote and accept. When signed accepts are required and a signer is enrolled, the quote response includes a signing block:
Pass data.signing.typed_data to the enrolled wallet. Do not use the register/challenge signature here.

Accept

Accept success shape matches the empty-body path — see Accept success.
If accept fails after signature verification, request a new quote. The nonce is consumed; there is no same-quote resume in v1. Idempotent retry of the same quote_id (before that failure path) still returns the existing order.

5. Constraints

  • EVM only in v1 (not Stellar)
  • Nonce = replay protection across quotes
  • Never forge typed_data — always pass Element Pay’s payload to the wallet
  • Provider-neutral: same flow for every corridor rail
  • Desk / accept-on-behalf fails when Signed accepts is on for that key

6. Failure cheat-sheet

Next