Quickstart
Your application never touches the blockchain: it creates an invoice, sends the customer to the payment page and unlocks access on a webhook. The three steps below cover the whole path from zero to the first payment.
Keys, products and the webhook endpoint are created in the dashboard (or through the API — see the sections below). The key itself picks the mode: sk_test_ or sk_live_. test
curl -X POST https://api.builtwide.dthreads.am/v1/checkout/sessions \
-H "Authorization: Bearer $SK" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"priceId":"PRICE_ID",
"endUserRef":"company:42",
"payerEmail":"client@example.com",
"metadata":{"userId":"42"}
}'Authentication
Every request to /v1 is authorised with the header Authorization: Bearer sk_test_….
- The secret key lives on your server only. Never ship it to a browser, a mobile app or a public repository.
- There is no mode switch in the API — the key prefix decides test vs live.
- Errors always have the same shape:
{"code":"<machine_code>"}. Unknown body fields are silently dropped.
curl https://api.builtwide.dthreads.am/v1/products \
-H "Authorization: Bearer sk_test_…"
# 401 → {"code":"invalid_api_key"}
# 429 → {"code":"rate_limited"}
# 422 → {"code":"validation_error","params":[{"param":"…","message":"…"}]}Products and purchase options (Price)
A Product is what you sell. A purchase option defines its type and amount; the API calls this immutable resource Price.
amount— a decimal USDC string from 0.50 to 50000; outside that range you get 422 amount_out_of_range.- A created purchase option (Price) cannot be edited: payment links may already point at it.
PATCH /v1/prices/{id}only switches it on and off (active). To change the amount, create a new purchase option and disable the old one. Disabling a Product disables all of its purchase options. type:one_time|credits_pack(needscreditsAmount) |subscription(needsperiodDays).periodDaysonly accepts a fixed set of values:7,30,90,365. Any other number (e.g. 31) returns 422 invalid_period_days.billingPeriod— calendar term instead of periodDays:month|year. Renews on the same day next month or year; the 31st clamps to the last day of shorter months. Sending both fields returns 422 period_conflict.amount— a decimal string in USD, up to 6 decimals. The response also returns amountMicro — an integer in micro-USDC.idof a Price purchase option is thepriceIdfor a checkout session.publicIdpowers a ready-made payment linkhttps://builtwide.dthreads.am/l/{publicId}. Internal Product and Price ids are UUID v4; a purchase-option publicId is price_ plus 24 lowercase hex characters.- Errors:
404 product_not_found,422 validation_error,422 invalid_period_days,422 amount_out_of_range,422 credits_amount_required(credits_pack without creditsAmount).
curl -X POST https://api.builtwide.dthreads.am/v1/products \
-H "Authorization: Bearer $SK" \
-H "Content-Type: application/json" \
-d '{"name":"Pro access","description":"Monthly customer subscription"}'
# 201 → {"id":"7a3f21c8-9d64-4b0e-8f52-1c7e4a9d0b31","name":"Pro access","description":"Monthly customer subscription",
# "active":true,"livemode":false,"createdAt":"2026-07-25T00:00:00.000Z"}curl -X POST https://api.builtwide.dthreads.am/v1/products/PRODUCT_ID/prices \
-H "Authorization: Bearer $SK" \
-H "Content-Type: application/json" \
-d '{"type":"subscription","amount":"19.99","periodDays":30}'
# 201 → {"id":"16bd0ac5-5891-42fd-86e7-b99479bc18c8","publicId":"price_738d8ecffdf10fba91f60963",
# "productId":"7a3f21c8-9d64-4b0e-8f52-1c7e4a9d0b31","type":"subscription",
# "amount":"19.990000","amountMicro":"19990000","currency":"usdc","chain_id":84532,
# "creditsAmount":null,"periodDays":30,"active":true}
# все цены продукта
curl https://api.builtwide.dthreads.am/v1/products/PRODUCT_ID/prices \
-H "Authorization: Bearer $SK"Checkout session
Create it server-side, always with an Idempotency-Key header: a repeated request returns the same invoice instead of creating a second one. The response carries session_id, invoice_id and url; redirect the customer to url.
{
"session_id": "4f70a63a-ba63-4795-9d70-161fb9953708",
"invoice_id": "inv_23b0c60736e92212",
"url": "https://builtwide.dthreads.am/i/inv_23b0c60736e92212"
}Body fields: priceId (required), endUserRef, payerEmail, successUrl, cancelUrl, metadata (an object up to 4 KB), expiresInSec (clamped into 900…86400). successUrl and cancelUrl accept http/https only, no IP hosts, and must match the origin you registered in the dashboard settings. The origin is checked whenever checkout loads, so removing it immediately hides return links in existing sessions too. After payment, successUrl is shown as the return to the seller; before payment, cancelUrl is shown as an exit to the store without paying. Checkout adds invoice_id to both links.
It is your own customer identifier inside your application — the platform uses it to tie payments, credits and subscriptions of the same person or company together. It does NOT have to be an email: company:42, tg:123, user_9f3 all work — any stable string. If you pass an email it is normalised to email:<lowercase>.
For subscription and credits_pack prices endUserRef is mandatory — without it the request returns 422 end_user_ref_required. For one_time it is optional, but useful for reconciliation.
403 receiving_address_not_verified/403 merchant_suspended/403 public_business_name_required/403 public_support_email_required/403 public_refund_policy_required- A public business name, separate support email, and refund-policy URL or text are required for live checkout. The sign-in email is never exposed; configure the profile in Settings → Checkout identity.
404 price_not_found409 idempotency_in_progress— the same key is still being processed, retry later422 price_inactive,422 end_user_ref_required,422 idempotency_key_reused422 redirect_origin_not_allowed— successUrl or cancelUrl points to a domain that isn't on the allowed list. Add the domain in the dashboard: Settings → Checkout identity → Post-payment return origins.
Webhooks
Webhooks are the primary way to learn about a payment. Register an endpoint in the dashboard and you get a whsec_… secret (shown once). One endpoint receives ALL event types of its mode; up to 5 endpoints per mode.
{
"id": "evt_…",
"type": "invoice.paid",
"created": 1730000000,
"livemode": false,
"api_version": "2026-07-01",
"data": {
"object": {
"invoice_id": "inv_724742c530d56c27",
"priceId": "…",
"paymentPath": "wallet",
"currency": "usdc",
"chain_id": 84532,
"amountMicro": "49000000",
"feeMicro": "490000",
"livemode": false,
"endUserRef": "email:buyer@example.com",
"payerEmail": "buyer@example.com",
"txHash": "0x…",
"metadata": { "orderId": "A-1" }
}
}
}Two ways to tie an event to your own order: by invoice_id — the same id the checkout session returned and that GET /v1/invoices/{id} accepts — and by the metadata you passed when creating the session.
Every invoice event (paid, underpaid, expired, canceled and refunded) includes currency and the numeric chain_id. Do not infer the currency or network from the amount or key mode; use the fields from the event itself.
The signature arrives in the Stablebill-Signature header, shaped t=<unix>,v1=<hex>. The signed string is ${t}.${rawBody} with HMAC-SHA256 keyed by whsec_…, where rawBody is the raw request body as received (do not re-serialise the JSON). During a secret rotation the header can carry several v1= values — old and new; matching any one of them is enough.
For routing and logs, the request also carries Stablebill-Event-Id and Stablebill-Event-Type. They mirror id and type from the body, but never replace Stablebill-Signature verification against the raw body.
const crypto = require("crypto");
function verify(req, whsec) {
const header = req.get("Stablebill-Signature") || "";
const parts = Object.fromEntries(header.split(",").map(kv => kv.split("=")));
const t = parts.t;
const raw = req.body; // Buffer from express.raw({ type: "application/json" })
const expected = crypto.createHmac("sha256", whsec)
.update(`${t}.${raw.toString("utf8")}`).digest("hex");
const provided = header.split(",").filter(p => p.startsWith("v1=")).map(p => p.slice(3));
const ok = provided.some(v =>
v.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected)));
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300; // anti-replay
return ok && fresh;
}invoice.paidinvoice.expiredinvoice.refundedinvoice.canceledinvoice.underpaidcredits.low_balancesubscription.activatedsubscription.renewedsubscription.amendedsubscription.expiringsubscription.expiredorphan.detectedbilling.renewal_duebilling.past_duebilling.pausedinvoice.paid— a one-off purchase or top-up is paid: deliver the goods or access.subscription.activated/subscription.renewed— start or extend the customer subscription.subscription.amended— the plan changed: a top-up was confirmed, or a renewal applied a scheduled downgrade. The period end does not move.billing.renewal_due— the merchant’s BuiltWide plan is about to end; this is not a buyer-subscription event.subscription.expiring— the customer subscription is ending: use this event for your own notifications when BuiltWide has no buyer email. Sent three days before and again in the final hour — the reminder field says which.subscription.expired— revoke access.orphan.detected— money arrived but it is unclear which invoice it settles (the amount did not match, the invoice had expired, or two invoices share the address). The order is paid but not marked: resolve it in the dashboard. The body carries the chain, transaction, log index and the address the money landed on.
Delivery is at-least-once: up to 7 attempts with a 0 → 30 s → 2 min → 10 min → 1 h → 6 h → 24 h backoff. Any 2xx from your endpoint counts as success.
Deduplicate on your side by the event id (evt_…) — it stays stable across retries and manual replays. Once evt_X is handled, ignore it next time.
Customer subscriptions
There are no automatic charges and there cannot be: the model is non-custodial, the platform has no access to the customer wallet. A subscription means manual renewal — the customer pays again each period.
- A customer subscription is created after a Price purchase option with type=subscription is paid — subscription.activated arrives.
billing.renewal_dueconcerns the merchant’s own BuiltWide plan. Buyer subscriptions emit subscription.expiring three days before expiry and in the final hour. BuiltWide emails a renewal link when endUserRef is email:address; for other references your application sends the reminder. Email delivery requires the configured mail service.- Renewed → subscription.renewed. Missed the deadline → subscription.expired. The billing.past_due event concerns an unpaid renewal of the merchant’s own BuiltWide plan.
curl "https://api.builtwide.dthreads.am/v1/subscriptions?endUserRef=company:42&status=active" \
-H "Authorization: Bearer $SK"
# 200 → {"data":[{"id":"2f9c48d1-0b73-4e6a-9c15-6d8b2e5f7a04","price_id":"16bd0ac5-5891-42fd-86e7-b99479bc18c8","end_user_ref":"company:42",
# "status":"active","expires_at":"2026-08-24T00:00:00.000Z",
# "current_period_start":"2026-07-25T00:00:00.000Z","anchor_day":25}]}
# anchor_day is the day of the month a calendar term runs on, and subscription.activated /
# subscription.renewed carry it too. A renewal paid while the term is still running keeps the day;
# a renewal paid after the term expired moves it to the day of that payment, because the new term
# starts then and the buyer gets a full month for a full price. Build monthly quotas on this day.
curl -X POST https://api.builtwide.dthreads.am/v1/subscriptions/SUBSCRIPTION_ID/renew-link \
-H "Authorization: Bearer $SK"
# 201 → {"url":"https://builtwide.dthreads.am/i/inv_23b0c60736e92212"}
# Stop the renewal: paid access runs to its end, the payment is not refunded
curl -X POST https://api.builtwide.dthreads.am/v1/subscriptions/SUBSCRIPTION_ID/cancel \
-H "Authorization: Bearer $SK"
# 200 → {"status":"canceled","expires_at":"2026-08-24T09:00:00.000Z",
# "canceled_at":"2026-07-28T12:00:00.000Z"}
# subscription.canceled says access is not renewed; revoke it on subscription.expiredChanging the plan of a live subscription goes through a quote and a confirmation. The quote moves no money: it shows the top-up for the rest of the paid period (more expensive) or the date a cheaper plan starts applying. Confirming with an Idempotency-Key bills exactly the top-up, and the plan changes only after that payment is confirmed. Within one interval the period end stays put — that period is already paid. Switching between month and year is priced differently: the rest of a month cannot be stretched into a year, so the buyer pays for a whole new period and the unused rest of the old plan is credited; the period is then replaced, which new_period_end makes explicit. A downgrade leaves the current period on the old terms, and the next renewal link points at the new price.
# 1. What the change costs. Moves no money, and the answer is short-lived.
curl -X POST https://api.builtwide.dthreads.am/v1/subscriptions/SUBSCRIPTION_ID/amendments/quote \
-H "Authorization: Bearer $SK" -H "Content-Type: application/json" \
-d '{"targetPriceId":"PRICE_ID"}'
# 200 → {"subscription_id":"…","subscription_version":3,"kind":"upgrade",
# "amount":"15.000000","amount_micro":"15000000","currency":"usdc",
# "period_start":"2026-09-01T00:00:00.000Z","period_end":"2026-10-01T00:00:00.000Z",
# "effective_at":"2026-09-16T00:00:00.000Z","quoted_at":"2026-09-16T00:00:00.000Z",
# "quote_expires_at":"2026-09-16T00:15:00.000Z"}
# 2. Confirm it. Send back the version, the amount and quoted_at: the amount is checked as of
# quoted_at, so the quoted price holds for 15 minutes; if the subscription moved, it is refused.
curl -X POST https://api.builtwide.dthreads.am/v1/subscriptions/SUBSCRIPTION_ID/amendments \
-H "Authorization: Bearer $SK" -H "Idempotency-Key: upgrade-company-42-october" \
-H "Content-Type: application/json" \
-d '{"targetPriceId":"PRICE_ID","expectedVersion":3,"expectedAmountMicro":"15000000","quotedAt":"2026-09-16T00:00:00.000Z"}'
# 201 → {"amendment_id":"sam_…","status":"awaiting_payment","invoice_id":"inv_…",
# "payment_url":"https://builtwide.dthreads.am/i/inv_…"}
# Repeating the same Idempotency-Key returns the same amendment and the same invoice.
# A downgrade needs no payment: status "scheduled", invoice_id null, payment_url null.
# 3. The plan changes on subscription.amended, not on invoice.paid alone. The same evidence
# is readable at any time — by id, or by the key you used, after a lost response.
curl https://api.builtwide.dthreads.am/v1/subscriptions/SUBSCRIPTION_ID/amendments/sam_… -H "Authorization: Bearer $SK"
curl "https://api.builtwide.dthreads.am/v1/subscriptions/SUBSCRIPTION_ID/amendments?idempotencyKey=upgrade-company-42-october" \
-H "Authorization: Bearer $SK"
# 4. Drop a change that has not been applied (a scheduled downgrade, an unpaid top-up).
curl -X POST https://api.builtwide.dthreads.am/v1/subscriptions/SUBSCRIPTION_ID/amendments/sam_…/cancel \
-H "Authorization: Bearer $SK" -H "Content-Type: application/json" \
-d '{"expectedVersion":4}'
# Refunding a top-up rolls the plan back to the previous price and leaves the period end
# alone: that period was paid by another invoice. An underpaid top-up changes nothing.
# Switching between month and year is priced differently, because the rest of a month cannot be
# stretched into a year: the buyer pays for a whole new period and the unused rest of the old plan
# is credited. The quote shows both parts, and new_period_end says the period moves:
# {"kind":"upgrade","credit_micro":"9500000","charge_micro":"190000000","amount_micro":"180500000",
# "period_end":"2026-10-01T00:00:00.000Z","new_period_end":"2027-09-16T00:00:00.000Z"}
# Paying that invoice replaces the period and moves the anchor day to the day of payment. Going the
# other way (year to month) the credit usually covers the cheaper plan, so it becomes a downgrade
# that waits for the paid year to run out — money is never returned for the surplus.Reconciliation
For reconciliation against your own billing table there is an authenticated invoice list and a single-invoice lookup. Both see only your merchant's invoices, and only in the key's mode (test or live).
limit— 1…100, defaults to 50.status—pending,confirming,underpaid,paid,expired,canceled,refunded.cursor— the id of the last item on the previous page. The list response is always {data, has_more}: has_more=true means one more page is waiting.- In /v1 amounts come in pairs: amount as a decimal string, amount_micro as an integer in micro-USDC — use amount_micro for arithmetic. In /public, amount, received and remaining are decimal USDC strings without the _micro suffix.
price_idis the internal price id, not the publicId used in the payment link /l/{publicId}. To match it against your own price, compare it to the id field returned by POST /v1/products/{id}/prices.POST /v1/invoices/{id}/cancelcancels only your merchant's pending invoice in the current API key mode. cancelUrl does not change the status by itself: it is the buyer's safe return link, while the decision to close the order remains with your server.
If an incoming USDC transfer cannot be matched to an invoice automatically, it appears in the dashboard under Payments → Transfers without an invoice in the same test/live mode. An Owner or Support member can review its transaction hash, sender and amount, then link it to a specific invoice_id with evidence or close it manually with a required explanation. Linking marks the invoice paid and runs the usual webhooks, credits, subscription and receipt effects.
curl "https://api.builtwide.dthreads.am/v1/invoices?limit=50&status=paid" \
-H "Authorization: Bearer $SK"
# 200 → {"data":[{"id":"inv_…","status":"paid","amount":"19.990000",
# "amount_micro":"19990000","received":"19.990000","overpaid":null,
# "currency":"usdc","chain_id":84532,"price_id":"16bd0ac5-5891-42fd-86e7-b99479bc18c8",
# "end_user_ref":"company:42","payer_email":null,"payment_path":"wallet",
# "tx_hash":"0x…","paid_at":"2026-07-25T00:00:00.000Z",
# "created_at":"2026-07-25T00:00:00.000Z","livemode":false}],
# "has_more":false}
# следующая страница: cursor = id последнего элемента предыдущей
curl "https://api.builtwide.dthreads.am/v1/invoices?limit=50&cursor=inv_…" -H "Authorization: Bearer $SK"
curl https://api.builtwide.dthreads.am/v1/invoices/INVOICE_ID -H "Authorization: Bearer $SK"
# 404 → {"code":"invoice_not_found"}# Public, no key: what the payer's page polls while waiting.
curl https://api.builtwide.dthreads.am/public/invoices/INVOICE_ID/status
# 200 → {"status":"pending","received":"0.000000","remaining":"49.000000","history":[]}Exchange payments
The buyer does not need a connected wallet: on checkout they choose exchange payment and receive the invoice address, Base network, USDC token and a round amount. Funds land on that address and the contract behind it forwards them to your verified wallet — the platform never holds them and cannot redirect them.
- Your server normally does not call this public endpoint: redirecting to the checkout-session url is enough. It is documented for custom checkout clients and debugging.
- The buyer sends the invoice amount to deposit_address on Base before expires_at. The address belongs to this invoice, so any transfer to it is matched to that invoice; paid requires the confirmed received total to reach the amount threshold. Partial transfers accumulate; below the threshold the invoice remains underpaid. The shortfall tolerance is min(2, max(0.30, amount × 1%)) USDC. A 5 USDC invoice settles from 4.70 (a 6% shortfall); a 49 USDC invoice settles from 48.51. The tolerance reduces the seller’s actual proceeds; overpayments and late transfers have separate handling.
- The minimum invoice is 5 USDC; a smaller amount returns 422 amount_below_exchange_min. Exchange in Live mode always works: the first 500 USDC of volume carries no fee, past that a percentage is taken from each payment. A BuiltWide plan removes that fee. Card belongs to the BuiltWide plan and is available only while /public/onramp/availability reports it as live.
- Repeated activation returns the same payment details idempotently. Once confirmed, the payment has payment_path=exchange and emits the normal invoice.paid event.
curl -X POST https://api.builtwide.dthreads.am/public/invoices/INVOICE_ID/exchange-mode
# 200 → {"pay_to":"0x…","amount":"49.000000","deposit_address":"0x…",
# "network":"base","token":"USDC","expires_at":1784985600}Test integration
To verify the full integration without real USDC, create a checkout session with an sk_test_ key, keep its invoice_id, and complete it through the public test endpoint. The invoice becomes paid and triggers the same webhooks and product effects as a real payment.
- First enable test-mode webhook delivery and evt_… deduplication, then call simulate and wait for invoice.paid. For credits_pack also check the balance; for subscription, check subscription.activated.
- A live invoice cannot be simulated: the API returns 403 test_payment_live_forbidden. An expired or canceled test invoice returns 409 test_payment_unavailable. Repeating an already paid test invoice is safe.
# INVOICE_ID is the invoice_id returned by a sk_test_ checkout session
curl -X POST https://api.builtwide.dthreads.am/public/invoices/INVOICE_ID/simulate
# 200 → {"paid":true,"status":"paid"}
# The normal invoice.paid / credits / subscription webhooks are emitted. Their effects — credits
# balance, subscription — land a moment later, when the worker drains the event; poll or wait for
# the webhook instead of reading straight after this call.Refunds
A refund is a verifiable USDC transfer from your current verified receiving wallet. Starting it immediately reverses granted credits or shortens the subscription, but the invoice becomes refunded and invoice.refunded is emitted only after the on-chain transfer is verified.
- Send a non-empty reason and keep the refund_id. Repeating it for the same invoice returns 409 refund_already_exists with the existing refund_id.
- For a wallet payment the response already contains the original payer address. For an exchange payment, never refund the exchange's shared hot wallet: send the buyer the one-time address_collection_url, collect their address and wait for awaiting_transfer.
- Send exactly amount_usdc on Base from the verified receiving wallet and submit its tx_hash. Poll confirming with GET; completion emits invoice.refunded. List responses use {data, has_more}.
- In test, perform the same approval and then POST …/simulate: no funds move, but the complete lifecycle and event are exercised. A live refund cannot be simulated.
If the first approval response was lost, repeat it and recover the existing refund_id from 409 refund_already_exists. Replace a lost or expired address link with POST /v1/refunds/{refundId}/regenerate-link: the old token stops working immediately, and replacement is allowed only before the buyer address is locked. A configured mail service also sends the new link when the invoice has a buyer email.
Approval and payout are separate states. Until invoice.refunded, poll GET /v1/refunds/{refundId} and reconcile entitlements through the credits balance or subscriptions list. Your application must account for rights reversed at approval. The public API does not expose refund cancel or reject; prolonged pending, failed or escalated states need support review. A request timeout alone must not restore access or trigger a second transfer.
curl -X POST https://api.builtwide.dthreads.am/v1/invoices/INVOICE_ID/refund \
-H "Authorization: Bearer $SK" \
-H "Content-Type: application/json" \
-d '{"reason":"Customer request"}'
# Approving a refund revokes what that invoice paid for right away: the subscription end is
# recomputed from the periods that are still paid and unrefunded, and credits granted by the
# invoice are reversed. If the transfer is rejected or the refund is canceled, both come back.
# Keep the refund_id and follow the instruction returned by the API.
curl https://api.builtwide.dthreads.am/v1/refunds/REFUND_ID -H "Authorization: Bearer $SK"
# After sending the exact USDC refund from your verified receiving wallet:
curl -X POST https://api.builtwide.dthreads.am/v1/refunds/REFUND_ID/transaction \
-H "Authorization: Bearer $SK" \
-H "Content-Type: application/json" \
-d '{"tx_hash":"0x…64 hex characters…"}'
# Sandbox only: complete the same lifecycle without moving funds.
curl -X POST https://api.builtwide.dthreads.am/v1/refunds/REFUND_ID/simulate \
-H "Authorization: Bearer sk_test_…"
# The invoice carries its refund: GET /v1/invoices/INVOICE_ID returns refund.completed_at —
# null while the transfer is pending, set once it is confirmed on chain.Credits
Credits are your own integer usage units tied to endUserRef; they are not USDC. Paying a credits_pack price grants them automatically. Manual grant and consume operations cover adjustments and usage inside your application.
- grant and consume accept a positive integer amount and always require a unique Idempotency-Key. Retrying the same request is safe; reusing the key with a different body returns 422 idempotency_key_reused.
- consume never takes the balance below zero: insufficient balance returns 402 insufficient_credits. Use the returned balance as the confirmed new balance.
- balance and ledger require the same normalised endUserRef. The ledger paginates with startingAfter and returns {data, has_more}; limit is 1…100.
- The credits.low_balance event warns about your configured threshold and may include top_up_offer with the selected package public price id; reconcile the actual balance and history through the API.
curl -X POST https://api.builtwide.dthreads.am/v1/credits/grant \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: grant-order-0143" \
-H "Content-Type: application/json" \
-d '{"endUserRef":"customer-0143","amount":1000}'
# 201 → {"balance":1000}
curl -X POST https://api.builtwide.dthreads.am/v1/credits/consume \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: consume-request-0143" \
-H "Content-Type: application/json" \
-d '{"endUserRef":"customer-0143","amount":25}'
# 200 → {"balance":975}
curl "https://api.builtwide.dthreads.am/v1/credits/balance?endUserRef=customer-0143" \
-H "Authorization: Bearer $SK"
curl "https://api.builtwide.dthreads.am/v1/credits/ledger?endUserRef=customer-0143&limit=20" \
-H "Authorization: Bearer $SK"
# 200 → {"data":[{"id":"cled_…","delta":1000,"reason":"manual",…}],"has_more":false}
# reason: manual — выдали или списали вручную этим API; purchase — начислено по оплате пакета.
# Continue with startingAfter=cled_… from the final row; ids are opaque.Payment exports
The CSV export streams only your merchant's payments in the API key's mode. It is intended for accounting reconciliation and does not require loading the full result into memory.
- from and to are optional ISO dates and both boundaries are inclusive. Without them the range runs from the beginning of the data through now; an invalid date returns 422 invalid_date.
- The response is text/csv as a payments.csv attachment. The currency column contains the record currency code in lowercase, matching the API; amount, fee and net are currency-neutral six-decimal money fields; payment_path distinguishes wallet, exchange and card.
curl --fail --output payments.csv \ "https://api.builtwide.dthreads.am/v1/exports/payments.csv?from=2026-07-01T00:00:00.000Z&to=2026-07-31T23:59:59.999Z" \ -H "Authorization: Bearer $SK" # invoice_id,created_at,paid_at,product,price,currency,amount,fee, # net,payment_path,tx_hash,payer_email,end_user_ref,status
Manual wallet signing
A regular browser wallet can connect and sign directly in the dashboard. Use the manual path only for a cold wallet, hardware device, or multisig interface.
The zero address and known platform/USDC addresses are rejected before a challenge is created. If Base has bytecode at the address, the dashboard shows a separate warning and requires explicit acknowledgement. Continue only for your own EIP-1271 contract wallet; never use a token contract or another application's address. If Base RPC is unavailable, no challenge is created—retry safely later.
- Enter the receiving address in Onboarding or Settings and request the signing message.
- Use Copy. Do not retype the message: every line break is part of the signature.
- For a regular wallet, sign the exact string with the same address using EIP-191 personal_sign. For a contract wallet, produce the signature in its multisig interface; the server verifies it through EIP-1271 on Base. This is a message signature, not a transfer.
- Paste the complete hex signature with its 0x prefix and choose Verify signature. When changing an existing address, repeat this for the current wallet and confirm with the current password. Confirm has a separate limit of 5 attempts per minute per IP; after rate_limited, wait for the next window.
Full API reference
The full specification of every endpoint, request body and error code lives in Swagger. The machine-readable version is the same contract as OpenAPI JSON: feed it to a client generator or to your AI agent.