← Back to API Docs

Customers

Withdraw for a user

Freeze the user balance locally, call POST /v1/user-withdrawals with chain and to_address, then settle on withdrawal.complete or unfreeze on withdrawal.failed.

Withdraw for a user

After a Customer exists and deposits have credited your merchant pool (and your local user ledger), you can send funds to the user’s own address with User Withdrawal.

This guide covers the Integration path: one API call with chain + to_address. It does not cover the merchant-portal Prepare → email → Hosted claim flow (prepare returns 403 portal_only on Integration).

Prerequisites: Create a customer, Webhooks.

1. Flow overview

User requests withdraw in your app
  → freeze local user balance
  → POST /v1/user-withdrawals (Secret)
  → status requested (merchant pool debited)
  → webhooks: requested → submitted → complete | failed
  → on complete: finalize local debit
  → on failed: unfreeze local balance
RuleDetail
Customer firstexternal_user_id must equal an existing Customer ref_id
Local ledger firstFreeze/debit your user balance before calling the API
Merchant poolWithdrawal spends your merchant crypto balance
Not MerchantPayoutSending to your cold wallet is a different product

2. Create a withdrawal

POST /v1/user-withdrawals
Auth: Secret + X-DoggiePay-Request-Id
Strongly recommended: Idempotency-Key (if it is a UUID, it may be used as withdrawal_id).

Request body

FieldTypeRequiredDescription
external_user_idstring (UUID)YesSame as Customer ref_id
amountstringYesPositive amount, e.g. "10.00"
currencystringYese.g. USDC
chainstringYesDestination chain alias, e.g. ETH
to_addressstringYesUser receive address (can differ each time)

Example

curl -sS https://api.doggiepay.com/v1/user-withdrawals \
  -H "Authorization: Bearer dp_live_sk_YOUR_SECRET" \
  -H "X-DoggiePay-Request-Id: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -H "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
  -H "Content-Type: application/json" \
  -d '{
    "external_user_id": "6ded84e7-5449-4a2b-9c1d-0afcce047b29",
    "amount": "10.00",
    "currency": "USDC",
    "chain": "ETH",
    "to_address": "0xCustomerWallet..."
  }'

Success data

FieldTypeDescription
withdrawal_iduuidWithdrawal id
statusstringTypically requested after accept

Common errors

HTTPerror.codeMeaning
400invalid_external_user_idNot a UUID
400invalid_to_addressMissing/invalid address
400below_min_withdrawalBelow platform minimum
400unsupported_currency / chain_or_asset_disabledAsset or chain not ready
404customer_not_foundCreate the Customer first
422insufficient_balanceMerchant pool balance too low
422user_withdrawal_failedOther business failure

3. Read status

MethodPathNotes
GET/v1/user-withdrawals/{id}Single withdrawal
GET/v1/user-withdrawals?external_user_id=&status=Filter list

Auth: Secret + Request-Id.

4. Webhooks and your ledger

typeWhenWhat you should do
withdrawal.requestedAccepted; merchant balance debitedOptional “processing”; local freeze usually already done
withdrawal.submittedSubmitted to the rail / chainOptional “broadcasting” UI
withdrawal.completeSuccess (often includes tx_hash)Confirm local debit / clear freeze
withdrawal.failedFailed; merchant balance restoredUnfreeze local user balance

Typical data fields: withdrawal_id, external_user_id, amount, currency, chain, to_address; submitted/complete may add chain fields; failed includes failure_reason.

Notes:

  • Portal Prepare can also emit withdrawal.prepared (no destination yet). Pure Integration sync creates will not see prepared.
  • Always verify HMAC — see Webhooks.

5. Do not call these on Integration

PathWhy
POST /v1/user-withdrawals/preparePortal-only (403 portal_only)
POST /v1/user-withdrawals/{id}/resend-claimPortal-only
Public claim Hosted /w/:idProduct path for portal email claim, not server Integration

Merchant self-custody outflows use MerchantPayout, not this API.

6. Checklist before go-live

  • Customer exists; external_user_id === ref_id
  • Freeze local balance before POST /v1/user-withdrawals
  • Idempotency-Key / Request-Id stable on retries
  • Handle insufficient_balance, below_min_withdrawal, customer_not_found
  • On complete finalize debit; on failed unfreeze
  • Secret key only on the server

Next: expand webhook handlers for deposits + withdrawals in one place (Webhooks).