Build the customer account area
Let signed-in customers see their services, pay invoices, add credit, save payment methods and edit their profile.
The account area is the part of your storefront a customer sees once signed in: their services, invoices, credit, saved payment methods and profile. Every call on this page goes to /portal/... with the customer's token as Authorization: Bearer $CUSTOMER_TOKEN, and answers with that customer's own records only.
Before you begin
Section titled Before you begin- Get a customer token as Sign customers in to your storefront describes.
- Serve an invoice page at
/billing/invoices/{invoice_id}on your Custom domain. Invoice emails link to it, and hosted checkouts return to it. - Payments go through Coritan's payment gateways for the customer's currency and country, or through your own PayNow store when your billing mode says so. See PayNow.
Show the customer's services
Section titled Show the customer's servicescurl "https://api.coritan.com/api/v1/orgs/acme/portal/services?per_page=20" \
-H "Authorization: Bearer $CUSTOMER_TOKEN"
The list is newest first. status_filter takes one of the service statuses, page and per_page (1–200, default 50) page through it, and consistency=eventual suits a poll that runs while the page is open.
Each service carries id, status, hostname, product_name, plan_name, billing_cycle, amount and next_due_date. awaiting_payment and open_invoice_id point at an unpaid invoice for it, and pending_plan_change at an upgrade waiting for payment. A provisioned service has a resource with its type, uuid, label, status, power and address. is_owner is false for a server another customer shared with this one. GET /portal/services/{service_id} returns one service in the same shape.
GET /portal/services/live?ids=41,42 says what the customer's servers are doing now: items with each service_id, kind and state, a checked_at time, and degraded when a server did not answer. We keep each answer for 10 seconds; add refresh=true to ask again.
For a game server behind a join address, PATCH /portal/services/{service_id}/gameproxy changes what players see in their server list: online_motd and offline_motd (up to 256 characters each), favicon, version_name, display_name, description, and status_cache_ttl_seconds (1–10). clear_favicon, clear_online_motd and clear_offline_motd remove a value.
Change a plan
Section titled Change a plan- List the plans a service can move to with
GET /portal/services/{service_id}/plans. They are sizes of the same product, for the same game and billing cycle, andcurrentmarks the plan it is on. Each has its pricingid,name,price,currency,ram_mb,vcpuanddisk_mb. - Show the cost with
GET /portal/services/{service_id}/plan-preview?org_pricing_id=88. The answer says whether the move is anupgrade, adowngradeorlateralinchange_type, what isdue_today, and for a downgrade whether credit will be issued (credit_will_be_issued,credit_estimate, andcredit_declined_becausewhen it will not). - Make the change with an
Idempotency-Key, so a retry does not ask twice:
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/services/41/change-plan" \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Idempotency-Key: 5c0e2a4e-plan-88" \
-H "Content-Type: application/json" \
-d '{"org_pricing_id": 88}'
An upgrade answers requires_payment: true with an invoice_id. The invoice charges the difference for the days left in the cycle, or a full cycle at the new price when no days are left, and the new plan applies once it is paid. Asking for a different upgrade before paying cancels the first invoice. POST /portal/services/{service_id}/cancel-plan-change cancels an unpaid upgrade.
A downgrade or a move at the same price applies at once and answers "message": "Plan updated". Coritan sets whether your organization credits the unused days of a downgrade, and every credit keeps back at least one day.
Cancel a service
Section titled Cancel a service- Show what the customer gets back with
GET /portal/services/{service_id}/cancel-preview?immediate=true. The answer carrieswill_creditandcredit_amount,declined_becausewhen there is no credit, andfalls_back_to_freewith thefree_plan_namewhen the server moves to your free plan instead of ending. - Cancel:
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/services/41/cancel" \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"immediate": false, "reason": "Moving to a bigger plan elsewhere"}'
| Field | Meaning |
|---|---|
immediate |
false (the default) keeps the service until its next due date. true ends it now. |
reason |
Why the customer is leaving. It goes in your audit log. |
keep_snapshot |
Container Apps only. true (the default) saves a snapshot before the server goes. |
keep_ipv4 |
Cloud Compute only. true keeps the public IPv4 address on the account at its own price. |
delete_server |
true deletes the server even when your organization sells a free plan. It needs immediate: true. |
We cancel the service's open invoices and email the customer. When your organization sells a free plan, a cancelled container moves to it and keeps its files, at once or at the end of the term. Coritan sets whether an immediate cancellation credits the unused days; an end-of-term cancellation uses every day it paid for. The answer has the status, the termination_date, the credit_issued, snapshot_taken and moved_to_free.
Show invoices
Section titled Show invoicesGET /portal/invoices lists the customer's invoices, newest first. status_filter takes one of the invoice statuses, or open for unpaid and overdue together. q searches invoice numbers. limit and offset page through the list, and with_total=true wraps it as items, total and counts for each status.
GET /portal/invoices/{invoice_id} adds the payments taken against the invoice, payment_processing while a slower payment method settles, and for an open invoice retry: when we will next try the saved payment method (next_retry_at), and how many attempts of max_attempts have run. GET /portal/invoices/{invoice_id}/pdf downloads the invoice as a PDF.
Take a payment
Section titled Take a paymentChoose the way that suits your page. Each works on an unpaid or overdue invoice.
- Hosted checkout
POST /portal/invoices/{invoice_id}/checkoutanswers acheckout_urlto send the customer to. It follows the same rules asPOST /storefront/checkout/{invoice_id}; see Pay an invoice.POST /portal/invoices/{invoice_id}/paydoes the same.GET /portal/invoices/{invoice_id}/gateways?currency=EURlists the gateways that can take the payment.- A saved payment method
POST /portal/invoices/{invoice_id}/charge?payment_method_id=7charges a saved method, or the default one withoutpayment_method_id. When the bank asks the customer to confirm, the answer isrequires_actionwith aclient_secret; after the customer confirms, callPOST /portal/invoices/{invoice_id}/confirm-payment?gateway_name=...&payment_intent_id=....- A card form in your page
GET /portal/payment-configreturns each gateway'spublic_config, such as the publishable key your card form needs.POST /portal/invoices/{invoice_id}/pay-intentreturns aclient_secretfor the form, withsave_method=trueto keep the card for renewals. Once the customer has paid, callconfirm-paymentas above.- PayPal buttons
- Save the approved order with
POST /portal/invoices/{invoice_id}/paypal-stash?order_id=...&gateway_name=..., then take the money withPOST /portal/invoices/{invoice_id}/paypal-captureand the same parameters. - Account credit
POST /portal/invoices/{invoice_id}/apply-creditpays what it can from the customer's balance and answersamount_applied,fully_paidandamount_remaining.
GET /portal/invoices/{invoice_id}/checkout-session resumes a checkout the customer left, for example after opening a new tab. It answers open, and for an open checkout the gateway_name, client_secret or paypal_order_id, and when it expires_at.
Handle refund requests
Section titled Handle refund requestsA customer can ask for a paid invoice's money back, and your staff decide in the staff console.
- Call
GET /portal/invoices/{invoice_id}/refund-eligibilitybefore showing the form.eligiblesays whether the customer can ask;blocker_messagessays why not, andreason_codeslists the reasons to offer. - Send the request:
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/invoices/5021/refund-request" \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"reason_code": "downtime", "reason_text": "The server was offline for most of the weekend."}'
reason_code is not_delivered, duplicate_charge, downtime or other. reason_text takes up to 2,000 characters, and other needs at least 10. We email the customer that the request arrived.
An invoice takes one pending request at a time. Payments in cryptocurrency are never refunded, and neither are services ended for breaking the terms of service. A request within 24 hours of paying for a new service, or 48 hours for a renewal, is inside the refund window; a later one still reaches your staff, marked as outside it.
When your staff approve a request, the money goes back to the payment method or onto the customer's balance, and a full refund ends the service at once. GET /portal/refund-requests lists the customer's requests with their status: pending, approved, rejected, withdrawn or failed. POST /portal/refund-requests/{request_id}/withdraw withdraws one that is still pending.
Add credit
Section titled Add creditGET /portal/credit returns the customer's balance, which we hold in US dollars, and its history, newest first. Each entry has the amount, the balance_after, a description, its kind, and the invoice_id or org_service_id it belongs to. limit (up to 100) and offset page through the history.
A top-up takes an amount in US dollars from $5 to $10,000, charged in the customer's currency:
POST /portal/credit/topup/checkout?amount=25answers a hosted checkout. Withoutreturn_urlandcancel_url, the customer returns to/billing?paid=1or/billing?cancelled=1on your site.POST /portal/credit/topup/charge?amount=25&payment_method_id=7charges a saved method and answers thenew_balance.POST /portal/credit/topup/pay-intent?amount=25returns aclient_secretfor a card form. After the customer pays,POST /portal/credit/topup/confirm-payment?gateway_name=...&payment_intent_id=...adds the credit.POST /portal/credit/topup/paypal-capture?order_id=...&gateway_name=...captures an approved PayPal order and adds the credit.
GET /portal/transactions lists every payment, refund, chargeback, credit added and credit spent. type_filter takes payment, refund, chargeback, credit_add or credit_deduct, and with_total=true adds the counts.
Accept cryptocurrency
Section titled Accept cryptocurrencyGET /portal/crypto/assetslists what the customer can send: eachnetworkandasset, themin_deposit_usd, theconfirmationsa deposit waits for, and theapplied_rate_usd.GET /portal/crypto/plan?amount=25lists every way to pay that amount, cheapest first. Addinvoice_idto plan for what an invoice still owes.GET /portal/crypto/quote?network=base&asset=USDC&amount=25&invoice_id=5021says exactly how much to send. Withinvoice_id, the deposit pays that invoice when it arrives; without it, the deposit becomes credit.GET /portal/crypto/address?network=basereturns the customer's deposit address. It stays the same, and one address serves every network of the same family.POST /portal/crypto/checkwith{"network": "base"}looks for the payment now.GET /portal/crypto/depositslists deposits, including those still confirming, andGET /portal/crypto/deposits/{deposit_id}returns one.
Save payment methods
Section titled Save payment methods- Start with
POST /portal/payment-methods/setup?return_url=...&cancel_url=.... The answer has aredirect_urlto send the customer to, or aclient_secretfor a card form, and thesession_id. - When the customer comes back, call
POST /portal/payment-methods/confirm?gateway_name=...&session_id=.... The method becomes the default unless you sendset_as_default=false.
GET /portal/payment-methods lists the saved methods with their display_label, brand, last4, expiry and is_default. PUT /portal/payment-methods/{method_id}/default makes one the default, and DELETE /portal/payment-methods/{method_id} removes it.
We charge the saved method for open invoices on our own while the customer's auto_pay_enabled is true, which it is unless they turn it off.
Set the currency and country
Section titled Set the currency and countryGET /portal/currencieslists the currencies a customer can pay in, each with itsquote_currency,nameandrateagainst the US dollar.GET /portal/countrieslists the billing countries by region, with the oneselectedfor this customer and the onedetectedfrom the connection.GET /portal/geo-currencysuggests a currency from the visitor's country. It needs no token, so your pages can use it before sign-in.GET /portal/payment-preferencereturns the customer'spreferred_payment_currency,country_code,auto_pay_enabledand asuggested_currency.PATCHon the same path changes them:
curl -X PATCH "https://api.coritan.com/api/v1/orgs/acme/portal/payment-preference" \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"currency": "EUR", "country_code": "DE", "auto_pay_enabled": true}'
Credit stays in US dollars. The preferred currency decides what we charge the customer in, converted when they pay.
Edit the profile or close the account
Section titled Edit the profile or close the accountPATCH /portal/profile changes the profile exactly as PATCH /auth/me does; see Keep the session going.
To close the account, the customer confirms who they are again:
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/account/close" \
-H "Authorization: Bearer $CUSTOMER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"password": "a long passphrase", "reason": "No longer needed"}'
A customer who signs in only with a social provider sends reauth_token instead of password. Get one from GET /auth/oauth/{provider}/authorize?intent=reauth with a provider already connected to the account; the callback returns it as reauth_token.
Closing needs every service cancelled and no invoice unpaid or overdue. It removes the customer's name, email, phone, company, custom fields, social connections and password, ends every session, and cannot be undone. The answer is {"closed": true, "sessions_revoked": 2}.
Show notices
Section titled Show noticesGET /portal/notices returns the notices your staff published for this customer: those for everyone, and those for paying or free customers depending on whether this customer pays. Visitors who are not signed in get theirs from GET /storefront/notices.
Result
Section titled ResultCustomers can follow their services, pay and download invoices, keep a balance, save cards and leave, all from your own pages. Your staff see the same records in the staff console.
Troubleshooting
Section titled Troubleshooting401on every call- The customer token is missing, expired or belongs to another organization. See Troubleshooting for customer sign-in.
Invoice not found or already paid- The invoice is paid, cancelled or not this customer's. Read it again with
GET /portal/invoices/{invoice_id}. No payment gateway availableorNo Stripe gateway available- No gateway takes that currency for the customer's country. Try
USD, or list what works withGET /portal/invoices/{invoice_id}/gateways. 402with the gateway's message- The bank declined the payment. Ask the customer to try another method.
Plan changes must keep the current billing cycle- The chosen plan bills on another cycle. Offer only the plans
GET /portal/services/{service_id}/plansreturns. Plan change already in progress- A request with the same
Idempotency-Keyis still running. Wait for it rather than sending again. Service already cancelled- The service is ending or has ended.
The minimum top-up is $5.00- Send an
amountfrom 5 to 10,000. Pick one of the reasons listedorTell us briefly what happened- Send a
reason_codefromreason_codes. Withother, write at least 10 characters inreason_text. 409when closing an account- The
X-Close-Refused-Reasonheader says why:active_services,unpaid_invoicesoralready_closed. Cancel the services or settle the invoices first. 409from a crypto call- Deposits on that network are not available right now. Offer another network from
GET /portal/crypto/assets.