Skip to content
Coritan Docs

Build the customer account area

Let signed-in customers see their services, pay invoices, add credit, save payment methods and edit their profile.

View as Markdown

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.

  • 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.
Shell
curl "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.

  1. 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, and current marks the plan it is on. Each has its pricing id, name, price, currency, ram_mb, vcpu and disk_mb.
  2. Show the cost with GET /portal/services/{service_id}/plan-preview?org_pricing_id=88. The answer says whether the move is an upgrade, a downgrade or lateral in change_type, what is due_today, and for a downgrade whether credit will be issued (credit_will_be_issued, credit_estimate, and credit_declined_because when it will not).
  3. Make the change with an Idempotency-Key, so a retry does not ask twice:
Shell
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.

  1. Show what the customer gets back with GET /portal/services/{service_id}/cancel-preview?immediate=true. The answer carries will_credit and credit_amount, declined_because when there is no credit, and falls_back_to_free with the free_plan_name when the server moves to your free plan instead of ending.
  2. Cancel:
Shell
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.

GET /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.

Choose the way that suits your page. Each works on an unpaid or overdue invoice.

Hosted checkout
POST /portal/invoices/{invoice_id}/checkout answers a checkout_url to send the customer to. It follows the same rules as POST /storefront/checkout/{invoice_id}; see Pay an invoice. POST /portal/invoices/{invoice_id}/pay does the same. GET /portal/invoices/{invoice_id}/gateways?currency=EUR lists the gateways that can take the payment.
A saved payment method
POST /portal/invoices/{invoice_id}/charge?payment_method_id=7 charges a saved method, or the default one without payment_method_id. When the bank asks the customer to confirm, the answer is requires_action with a client_secret; after the customer confirms, call POST /portal/invoices/{invoice_id}/confirm-payment?gateway_name=...&payment_intent_id=....
A card form in your page
GET /portal/payment-config returns each gateway's public_config, such as the publishable key your card form needs. POST /portal/invoices/{invoice_id}/pay-intent returns a client_secret for the form, with save_method=true to keep the card for renewals. Once the customer has paid, call confirm-payment as above.
PayPal buttons
Save the approved order with POST /portal/invoices/{invoice_id}/paypal-stash?order_id=...&gateway_name=..., then take the money with POST /portal/invoices/{invoice_id}/paypal-capture and the same parameters.
Account credit
POST /portal/invoices/{invoice_id}/apply-credit pays what it can from the customer's balance and answers amount_applied, fully_paid and amount_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.

A customer can ask for a paid invoice's money back, and your staff decide in the staff console.

  1. Call GET /portal/invoices/{invoice_id}/refund-eligibility before showing the form. eligible says whether the customer can ask; blocker_messages says why not, and reason_codes lists the reasons to offer.
  2. Send the request:
Shell
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.

GET /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=25 answers a hosted checkout. Without return_url and cancel_url, the customer returns to /billing?paid=1 or /billing?cancelled=1 on your site.
  • POST /portal/credit/topup/charge?amount=25&payment_method_id=7 charges a saved method and answers the new_balance.
  • POST /portal/credit/topup/pay-intent?amount=25 returns a client_secret for 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.

  1. GET /portal/crypto/assets lists what the customer can send: each network and asset, the min_deposit_usd, the confirmations a deposit waits for, and the applied_rate_usd.
  2. GET /portal/crypto/plan?amount=25 lists every way to pay that amount, cheapest first. Add invoice_id to plan for what an invoice still owes.
  3. GET /portal/crypto/quote?network=base&asset=USDC&amount=25&invoice_id=5021 says exactly how much to send. With invoice_id, the deposit pays that invoice when it arrives; without it, the deposit becomes credit.
  4. GET /portal/crypto/address?network=base returns the customer's deposit address. It stays the same, and one address serves every network of the same family.
  5. POST /portal/crypto/check with {"network": "base"} looks for the payment now. GET /portal/crypto/deposits lists deposits, including those still confirming, and GET /portal/crypto/deposits/{deposit_id} returns one.
  1. Start with POST /portal/payment-methods/setup?return_url=...&cancel_url=.... The answer has a redirect_url to send the customer to, or a client_secret for a card form, and the session_id.
  2. When the customer comes back, call POST /portal/payment-methods/confirm?gateway_name=...&session_id=.... The method becomes the default unless you send set_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.

  • GET /portal/currencies lists the currencies a customer can pay in, each with its quote_currency, name and rate against the US dollar.
  • GET /portal/countries lists the billing countries by region, with the one selected for this customer and the one detected from the connection.
  • GET /portal/geo-currency suggests a currency from the visitor's country. It needs no token, so your pages can use it before sign-in.
  • GET /portal/payment-preference returns the customer's preferred_payment_currency, country_code, auto_pay_enabled and a suggested_currency. PATCH on the same path changes them:
Shell
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 account

PATCH /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:

Shell
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}.

GET /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.

Customers 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.

401 on 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 available or No Stripe gateway available
No gateway takes that currency for the customer's country. Try USD, or list what works with GET /portal/invoices/{invoice_id}/gateways.
402 with 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}/plans returns.
Plan change already in progress
A request with the same Idempotency-Key is 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 amount from 5 to 10,000.
Pick one of the reasons listed or Tell us briefly what happened
Send a reason_code from reason_codes. With other, write at least 10 characters in reason_text.
409 when closing an account
The X-Close-Refused-Reason header says why: active_services, unpaid_invoices or already_closed. Cancel the services or settle the invoices first.
409 from a crypto call
Deposits on that network are not available right now. Offer another network from GET /portal/crypto/assets.

API operations on this page

MethodPathWhat it does
POST/api/v1/orgs/{org_slug}/portal/account/closeClose and anonymise the signed-in customer's own account
GET/api/v1/orgs/{org_slug}/portal/countriesCountries a customer can bill from, and the one we think they are in
GET/api/v1/orgs/{org_slug}/portal/creditGet my credit balance
POST/api/v1/orgs/{org_slug}/portal/credit/topup/chargeCharge a saved payment method and credit the org customer balance (USD)
POST/api/v1/orgs/{org_slug}/portal/credit/topup/checkoutHosted checkout to buy USD credit for an org customer
POST/api/v1/orgs/{org_slug}/portal/credit/topup/confirm-paymentFinalize a credit top-up PaymentIntent after SCA and credit the balance
POST/api/v1/orgs/{org_slug}/portal/credit/topup/pay-intentCreate an in-page Stripe Payment Element intent for a credit top-up
POST/api/v1/orgs/{org_slug}/portal/credit/topup/paypal-captureCapture PayPal order and credit org customer balance
GET/api/v1/orgs/{org_slug}/portal/crypto/addressThis customer's permanent deposit address for the network's family
GET/api/v1/orgs/{org_slug}/portal/crypto/assetsAssets this storefront can accept right now
POST/api/v1/orgs/{org_slug}/portal/crypto/checkLook for this customer's payment now, and say where it stands
GET/api/v1/orgs/{org_slug}/portal/crypto/depositsDeposit history, including transfers still confirming
GET/api/v1/orgs/{org_slug}/portal/crypto/deposits/{deposit_id}One deposit, for polling while it confirms
GET/api/v1/orgs/{org_slug}/portal/crypto/planEvery way to pay this amount in crypto, cheapest first
GET/api/v1/orgs/{org_slug}/portal/crypto/quoteHow much to send for a target USD credit, itemised
GET/api/v1/orgs/{org_slug}/portal/currenciesList portal currencies
GET/api/v1/orgs/{org_slug}/portal/geo-currencyUnauthenticated country → suggested pay currency for storefront preselect
GET/api/v1/orgs/{org_slug}/portal/invoicesNewest first, ties by id
GET/api/v1/orgs/{org_slug}/portal/invoices/{invoice_id}Get my invoice
POST/api/v1/orgs/{org_slug}/portal/invoices/{invoice_id}/apply-creditApply the customer's credit balance toward an unpaid invoice
POST/api/v1/orgs/{org_slug}/portal/invoices/{invoice_id}/chargePay an invoice using a saved payment method (merchant-initiated charge)
POST/api/v1/orgs/{org_slug}/portal/invoices/{invoice_id}/checkoutCanonical hosted checkout for an org-customer invoice (alias of /pay)
GET/api/v1/orgs/{org_slug}/portal/invoices/{invoice_id}/checkout-sessionResume an in-flight checkout after a new tab, login, or API restart
POST/api/v1/orgs/{org_slug}/portal/invoices/{invoice_id}/confirm-paymentFinalize a charge after customer completes SCA (3-D Secure)
GET/api/v1/orgs/{org_slug}/portal/invoices/{invoice_id}/gatewaysGateway accounts that can checkout this invoice's pay currency
POST/api/v1/orgs/{org_slug}/portal/invoices/{invoice_id}/payHosted checkout for an invoice (legacy path; prefer /checkout)
POST/api/v1/orgs/{org_slug}/portal/invoices/{invoice_id}/pay-intentCreate an in-page Stripe Payment Element intent for an org invoice
POST/api/v1/orgs/{org_slug}/portal/invoices/{invoice_id}/paypal-captureCapture paypal order
POST/api/v1/orgs/{org_slug}/portal/invoices/{invoice_id}/paypal-stashPersist a PayPal order id so a redirect or reboot can still capture it
GET/api/v1/orgs/{org_slug}/portal/invoices/{invoice_id}/pdfThe invoice as a document to keep
GET/api/v1/orgs/{org_slug}/portal/invoices/{invoice_id}/refund-eligibilityMy invoice refund eligibility
POST/api/v1/orgs/{org_slug}/portal/invoices/{invoice_id}/refund-requestRequest my invoice refund
GET/api/v1/orgs/{org_slug}/portal/noticesLive notices for the signed-in customer, by whether they pay
GET/api/v1/orgs/{org_slug}/portal/payment-configGet portal payment config
GET/api/v1/orgs/{org_slug}/portal/payment-methodsList customer's saved payment methods
POST/api/v1/orgs/{org_slug}/portal/payment-methods/confirmConfirm payment method setup after customer approves on gateway side
POST/api/v1/orgs/{org_slug}/portal/payment-methods/setupInitiate payment method tokenization on a regional gateway account
DELETE/api/v1/orgs/{org_slug}/portal/payment-methods/{method_id}Remove (deactivate) a saved payment method
PUT/api/v1/orgs/{org_slug}/portal/payment-methods/{method_id}/defaultSet a payment method as the default for this customer
GET/api/v1/orgs/{org_slug}/portal/payment-preferenceGet portal payment preference
PATCH/api/v1/orgs/{org_slug}/portal/payment-preferenceUpdate account-wide preferred payment currency
PATCH/api/v1/orgs/{org_slug}/portal/profileAlias for PATCH /auth/me (account page compatibility)
GET/api/v1/orgs/{org_slug}/portal/refund-requestsList my refund requests
POST/api/v1/orgs/{org_slug}/portal/refund-requests/{request_id}/withdrawWithdraw my refund request
GET/api/v1/orgs/{org_slug}/portal/servicesList my services
GET/api/v1/orgs/{org_slug}/portal/services/liveWhat the customer's servers are doing right now, in one request
GET/api/v1/orgs/{org_slug}/portal/services/{service_id}Get my service
POST/api/v1/orgs/{org_slug}/portal/services/{service_id}/cancelCancel one of the customer's own services, now or at the end of term
POST/api/v1/orgs/{org_slug}/portal/services/{service_id}/cancel-plan-changeCancel an unpaid upgrade so a different plan can be chosen
GET/api/v1/orgs/{org_slug}/portal/services/{service_id}/cancel-previewWhat cancelling right now would return, before committing to it
POST/api/v1/orgs/{org_slug}/portal/services/{service_id}/change-planChange my service plan
GET/api/v1/orgs/{org_slug}/portal/services/{service_id}/community-listingGet community listing
PUT/api/v1/orgs/{org_slug}/portal/services/{service_id}/community-listingUpsert community listing
PATCH/api/v1/orgs/{org_slug}/portal/services/{service_id}/gameproxyUpdate join-address branding (MOTD/favicon) for a service's sidecar gameproxy route
GET/api/v1/orgs/{org_slug}/portal/services/{service_id}/plan-previewWhat a plan change costs today, before committing to it
GET/api/v1/orgs/{org_slug}/portal/services/{service_id}/plansSizes this service can move to
GET/api/v1/orgs/{org_slug}/portal/transactionsList my transactions