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
| Rule | Detail |
|---|---|
| Customer first | external_user_id must equal an existing Customer ref_id |
| Local ledger first | Freeze/debit your user balance before calling the API |
| Merchant pool | Withdrawal spends your merchant crypto balance |
| Not MerchantPayout | Sending 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
| Field | Type | Required | Description |
|---|---|---|---|
external_user_id | string (UUID) | Yes | Same as Customer ref_id |
amount | string | Yes | Positive amount, e.g. "10.00" |
currency | string | Yes | e.g. USDC |
chain | string | Yes | Destination chain alias, e.g. ETH |
to_address | string | Yes | User 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
| Field | Type | Description |
|---|---|---|
withdrawal_id | uuid | Withdrawal id |
status | string | Typically requested after accept |
Common errors
| HTTP | error.code | Meaning |
|---|---|---|
| 400 | invalid_external_user_id | Not a UUID |
| 400 | invalid_to_address | Missing/invalid address |
| 400 | below_min_withdrawal | Below platform minimum |
| 400 | unsupported_currency / chain_or_asset_disabled | Asset or chain not ready |
| 404 | customer_not_found | Create the Customer first |
| 422 | insufficient_balance | Merchant pool balance too low |
| 422 | user_withdrawal_failed | Other business failure |
3. Read status
| Method | Path | Notes |
|---|---|---|
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
type | When | What you should do |
|---|---|---|
withdrawal.requested | Accepted; merchant balance debited | Optional “processing”; local freeze usually already done |
withdrawal.submitted | Submitted to the rail / chain | Optional “broadcasting” UI |
withdrawal.complete | Success (often includes tx_hash) | Confirm local debit / clear freeze |
withdrawal.failed | Failed; merchant balance restored | Unfreeze 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 seeprepared. - Always verify HMAC — see Webhooks.
5. Do not call these on Integration
| Path | Why |
|---|---|
POST /v1/user-withdrawals/prepare | Portal-only (403 portal_only) |
POST /v1/user-withdrawals/{id}/resend-claim | Portal-only |
Public claim Hosted /w/:id | Product 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
completefinalize debit; onfailedunfreeze - Secret key only on the server
Next: expand webhook handlers for deposits + withdrawals in one place (Webhooks).
