Can I use this API today?
Not on this site yet. This deployment serves documentation only: every /v1 route returns HTTP 404 foundation_closed. The contract below is implemented and tested. It runs in development and test deployments with simulated infrastructure, and it was verified on real cloud servers in the EU in a private, operator-admitted environment with known test images. Public hosting, real payments and human account claim are not available. See pricing for the planned launch terms.
Quick reference
| Topic | Rule |
|---|---|
| Credential | Generate it yourself: eph_ plus 64 lowercase hex characters from 32 random bytes. Send Authorization: Bearer. Register it with POST /v1/bootstrap; there is no signup form. |
| Mutations | Every POST, PATCH and DELETE needs an Idempotency-Key. Retrying the same key and body returns the original response; a changed body under the same key returns 409. |
| Long work | Returns 202 with a status_url. Poll it. The operation is finished only when state is succeeded, failed or cancelled; phase is progress. |
| Money | EUR in integer micro-units (1 EUR = 1,000,000). Prepaid only. Each application has a lifetime spending ceiling and an optional stop time. |
| Images | Digest-pinned OCI references only (repo@sha256:…); tags are rejected. No source builds yet. |
| Errors | JSON envelope with error.code, message, retryable, details and next_action. See retries and errors. |
| MCP servers | A remote MCP server deploys like any other image: one HTTP process behind the application's HTTPS address. See how to host an MCP server for what launch adds. |
| Limits today | One application per principal, one HTTP process per application, a platform hostname only. |
Start here
These examples run against a development or test deployment of Euphoric, started with bin/dev (web process plus worker); on that deployment, hosting is simulated and credit is test-only. These examples use curl, OpenSSL, and jq. Keep the generated credential: it owns your sandbox resources. Download the OpenAPI specification.
export EUPHORIC_URL=http://localhost:3000
export EUPHORIC_TOKEN="eph_$(openssl rand -hex 32)"
curl -sS "$EUPHORIC_URL/v1/bootstrap" \
-H "Authorization: Bearer $EUPHORIC_TOKEN" \
-H 'Idempotency-Key: bootstrap-1' \
-H 'Content-Type: application/json' \
-d '{"contact":"agent@example.test"}'
An email address or HTTPS callback is accepted. Bootstrap leaves it unverified and sends nothing. Explicit verification is available through the contact API; delivery is disabled by default. Repeating bootstrap with the same credential and contact recovers the same principal.
Metered test hosting
The metered plan is the primary path. Development deployments offer test-runtime-v1. This plan meters allocated ready-instance time at 10 EUR micro-units/second (€0.036/hour, €0.864/day). These are test prices, not a commercial offer. No payments are collected. One euro is exactly 1,000,000 micro-units. The earlier fixed-reservation API remains available below; its grants and balances are separate and never converted into metered credit.
After bootstrap, read the plan and add metered test credit. Funding requires sandbox:credit; plan, balance and event reads require applications:read. Grant 1–3000 cents per request, up to €100 total per principal. Reuse the original key and body after an interrupted request; use a new key for each intentional additional grant.
curl -sS "$EUPHORIC_URL/v1/plans" \
-H "Authorization: Bearer $EUPHORIC_TOKEN"
curl -sS "$EUPHORIC_URL/v1/sandbox/funding" \
-H "Authorization: Bearer $EUPHORIC_TOKEN" \
-H 'Idempotency-Key: metered-fund-1' -H 'Content-Type: application/json' \
-d '{"amount_minor":1000}'
response=$(curl -sS "$EUPHORIC_URL/v1/applications" \
-H "Authorization: Bearer $EUPHORIC_TOKEN" \
-H 'Idempotency-Key: metered-app-1' -H 'Content-Type: application/json' \
-d '{"image":"registry.example/euphoric/hello@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","plan_id":"test-runtime-v1","spend_limit_micro":10000000}')
app_id=$(echo "$response" | jq -r .application_id)
status_url=$(echo "$response" | jq -r .status_url)
# Repeat this read until state is succeeded, failed, or cancelled.
curl -sS "$EUPHORIC_URL$status_url" \
-H "Authorization: Bearer $EUPHORIC_TOKEN"
Only one nondeleted application is allowed, including suspended applications. Delete any earlier demo before creating this one. With plan_id, spend_limit_micro is required: an integer from 3000 to 100000000, representing the application's lifetime ceiling, including charged and reserved runtime. Starting or resuming needs at least five minutes of credit and allowance. Optional stop_at is an ISO 8601 timestamp with timezone at least five minutes ahead on creation. It requests a stop; adding credit does not extend it.
Provisioning reserves five minutes before allocation. At instance readiness, billing starts and Euphoric reserves a rolling window targeting 24 hours, renewed every five minutes, limited by funds, the lifetime ceiling, stop time, and staging campaign expiry. The sandbox simulates this host authorization. Private staging requires an explicitly admitted funded-runtime campaign and a policy-enabled image; known-image live acceptance, including outage and recovery cases, passed there in September 2026.
curl -sS "$EUPHORIC_URL/v1/billing/balance" \ -H "Authorization: Bearer $EUPHORIC_TOKEN" curl -sS "$EUPHORIC_URL/v1/applications/$app_id" \ -H "Authorization: Bearer $EUPHORIC_TOKEN" curl -sS "$EUPHORIC_URL/v1/billing/events" \ -H "Authorization: Bearer $EUPHORIC_TOKEN"
The metered balance returns credited_micro, charged_micro, balance_micro, reserved_micro, available_micro, current_rate_micro_per_second, and nullable estimated_runway_seconds. Estimates use posted usage and current rates. Inspect the application's billing.metered_through for freshness and its ceiling/stop time for earlier limits. billing.authorized_until is the issued deadline; billing.acknowledged_until is the last host-confirmed deadline. Pending delivery does not establish extra outage tolerance. Reservations are commitments, not usage charges.
Events are persistent snapshots: payment.credited (a simulated grant here), balance.low, application.suspended, and application.deletion_scheduled. Low-runway warnings occur at 24 hours and one hour and rearm when runway rises above the threshold. Poll and deduplicate by event id. The latest 100 events are returned newest first; pass ?before=NEXT_BEFORE using next_before to read older pages. An inaccessible cursor returns 404. No email or callback is sent.
To suspend, wait until provisioning and any release/export operation have finished. Both suspend and resume require applications:write and return a 202 operation to poll. Confirmed suspension releases unused runtime commitments and starts 72-hour retention without runtime charges. Configuration exports remain available. Then a durable deletion operation removes resources; unresolved cleanup retains its resource reservations. A staging campaign's independent expiry may clean up sooner.
curl -sS -X POST "$EUPHORIC_URL/v1/applications/$app_id/suspend" \
-H "Authorization: Bearer $EUPHORIC_TOKEN" -H 'Idempotency-Key: suspend-1'
# Poll that operation before resuming. Add test funding first if needed.
curl -sS -X POST "$EUPHORIC_URL/v1/applications/$app_id/resume" \
-H "Authorization: Bearer $EUPHORIC_TOKEN" -H 'Idempotency-Key: resume-1'
# Raise the ceiling or remove a requested stop deadline. This does not resume an app.
curl -sS -X PATCH "$EUPHORIC_URL/v1/applications/$app_id/billing" \
-H "Authorization: Bearer $EUPHORIC_TOKEN" \
-H 'Idempotency-Key: billing-policy-1' -H 'Content-Type: application/json' \
-d '{"spend_limit_micro":20000000,"stop_at":null}'
A 402 means more credit or spending allowance is required. Resume returns 409 after retention expires, at an elapsed stop deadline, or in an incompatible state. Lowering the ceiling below charged plus reserved amounts returns 409. Shortening a stop deadline below an issued authorization also returns 409: suspend and await confirmation first. Increasing funding or limits never automatically resumes an application. When a host is unreachable, commitments remain reserved until its actual stop/expiry is reconciled; controller silence alone does not release them.
Application fields and naming
image is required. Omit name to generate a readable three-word name; explicit null or empty is invalid. A supplied name must be 3–40 characters, start with a lowercase letter, and contain only lowercase letters, digits, and hyphens: ^[a-z][a-z0-9-]{2,39}$. For example, hello-agent is valid; Hello Agent! is not. Names are unique within your principal. Slugs are globally unique and retained after deletion; renaming the display label never changes the slug.
image must be pinned to a SHA-256 digest: a repository reference followed by @sha256: and exactly 64 lowercase hexadecimal characters. Its maximum length is 512 characters, and its full pattern is ^[a-zA-Z0-9][a-zA-Z0-9._:/-]*@sha256:[a-f0-9]{64}$. A tag alone such as hello:latest is rejected with HTTP 422. The example below is a syntactically valid placeholder; the sandbox fetches and executes no image. One nondeleted application is allowed per principal, so delete the metered example first or reuse its ID.
response=$(curl -sS "$EUPHORIC_URL/v1/applications" \
-H "Authorization: Bearer $EUPHORIC_TOKEN" \
-H 'Idempotency-Key: app-1' \
-H 'Content-Type: application/json' \
-d '{"name":"hello-agent","image":"registry.example/euphoric/hello@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","plan_id":"test-runtime-v1","spend_limit_micro":10000000}')
app_id=$(echo "$response" | jq -r .application_id)
operation_id=$(echo "$response" | jq -r .operation_id)
curl -sS "$EUPHORIC_URL/v1/operations/$operation_id" \
-H "Authorization: Bearer $EUPHORIC_TOKEN"
The HTTP 202 create response contains application_id, operation_id, status_url, and phase: queued; it has no state field. Poll GET /v1/operations/:id at the returned status_url for completion.
| Field | Meaning |
|---|---|
Operation state | queued or running while work is pending. Stop polling at succeeded, failed, or cancelled. Only succeeded indicates success; inspect error on failure. |
Operation phase | The current step, such as allocating, checking, or ready. Use state to decide whether the operation is finished. |
Application state | The resource lifecycle: requested, provisioning, ready, suspending, suspended, deleting, or deleted. |
After successful provisioning, the operation has state: succeeded and phase: ready, while the application has state: ready and url: null. If the operation stays queued, check that bin/jobs is running.
Recover an application ID
GET /v1/applications returns an applications array containing your principal’s most recent 50 applications, newest first, including deleted records. Each entry includes id, name, and state. Use the same credential that created the application. List applications and recover the ID by name:
curl -sS "$EUPHORIC_URL/v1/applications" \ -H "Authorization: Bearer $EUPHORIC_TOKEN" app_id=$(curl -sS "$EUPHORIC_URL/v1/applications" \ -H "Authorization: Bearer $EUPHORIC_TOKEN" \ | jq -r '.applications[] | select(.name == "hello-agent") | .id') curl -sS "$EUPHORIC_URL/v1/applications/$app_id" \ -H "Authorization: Bearer $EUPHORIC_TOKEN"
GET /v1/applications/:id returns the application object directly. These reads require no idempotency key. If you also lost the operation ID, replay the original mutation with its original idempotency key and body to recover the accepted response. There is no operation-list endpoint or credential recovery endpoint in this slice.
Rename and replace a release
Rename only the display label with PATCH /v1/applications/:id. The UUID and slug stay fixed. Replace the image with POST /v1/applications/:id/releases, then poll its returned operation. Wait for one deployment to finish before requesting another. The local sandbox simulates replacement without executing an image.
curl -sS -X PATCH "$EUPHORIC_URL/v1/applications/$app_id" \
-H "Authorization: Bearer $EUPHORIC_TOKEN" \
-H 'Idempotency-Key: rename-1' -H 'Content-Type: application/json' \
-d '{"name":"hello-renamed"}'
curl -sS "$EUPHORIC_URL/v1/applications/$app_id/releases" \
-H "Authorization: Bearer $EUPHORIC_TOKEN" \
-H 'Idempotency-Key: release-1' -H 'Content-Type: application/json' \
-d '{"image":"registry.example/euphoric/hello@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}'
curl -sS "$EUPHORIC_URL/v1/applications/$app_id/releases" \
-H "Authorization: Bearer $EUPHORIC_TOKEN"
Release history lists at most 50 entries with immutable image, state, active, and operation_id. Reads require no idempotency key. Application detail also includes active_release.
Inspect, export, and delete
curl -sS "$EUPHORIC_URL/v1/applications/$app_id/logs" \ -H "Authorization: Bearer $EUPHORIC_TOKEN" curl -sS -X POST "$EUPHORIC_URL/v1/applications/$app_id/exports" \ -H "Authorization: Bearer $EUPHORIC_TOKEN" \ -H 'Idempotency-Key: export-1' # Poll the export operation; its result is the configuration manifest. # Delete after the export completes: curl -sS -X DELETE "$EUPHORIC_URL/v1/applications/$app_id" \ -H "Authorization: Bearer $EUPHORIC_TOKEN" \ -H 'Idempotency-Key: delete-1'
Logs are operation events; exports contain configuration only. Deletion cancels earlier unfinished operations and returns its own operation to poll. One active application is allowed, and names cannot be reused.
Retries and errors
Every authenticated customer mutation requires an Idempotency-Key; the provider webhook uses its dedicated signature instead. Repeating the same endpoint, key, and JSON body returns the original response. Use a new key for a changed request. Poll the operation for its current state rather than expecting a replayed response to change.
| HTTP | What to do |
|---|---|
| 400 / 415 | Check JSON, Content-Type, and the Idempotency-Key header. |
| 401 | The credential is missing, malformed, unregistered, or revoked. Use an active bootstrapped credential. |
| 403 | insufficient_scope: the message names the missing scope. Ask the operator who restricted the credential to restore access; re-bootstrapping the same credential does not reset scopes. |
| 402 | Add the applicable test credit or increase the metered spending ceiling before starting or resuming. |
| 404 | foundation_closed: this deployment serves documentation only; no customer route is open on it. not_found: check the path, HTTP method, resource ID, and owning credential. Unknown paths and unsupported methods under /v1 also return JSON errors. |
| 409 | Resolve the state, app limit, or idempotency conflict. |
| 413 / 422 | Reduce the request body or correct the named fields. |
| 503 | Inspect error.code: the selected mode may be unavailable, or staging runtime logs may be temporarily unavailable. Production enables neither sandbox nor staging. |
API errors use a JSON envelope with error.code, error.message, error.retryable, error.details, error.next_action, and a top-level request_id. For HTTP 422, error.details identifies the invalid fields.
Credentials and scopes
Bootstrap automatically grants all ten scopes below to a new credential. There is currently no public API for choosing, granting, or changing scopes. Existing stored credentials keep their previous scopes; use a fresh test principal for payment scopes. A scope-related 403 insufficient_scope occurs for older credentials or if an operator or test has restricted it; staging admission can independently return 403. Scopes never grant access to another principal’s resources.
| Scope | Required for |
|---|---|
applications:read | GET plans, balances and billing events; GET application list, detail, releases, and logs; POST application exports. |
applications:write | POST applications, releases, suspend and resume; PATCH display name and billing policy; DELETE an application. |
operations:read | GET operation status. |
sandbox:credit | POST sandbox credits (legacy) and sandbox funding (metered). |
payments:read | GET an owned payment order. |
payments:write | POST payment orders and pay initiation. |
contact:read | Contact and admission inspection. |
contact:write | Explicit verification request and confirmation. |
safety:read | Own published notices and appeal outcomes. |
safety:write | Appeal a current restrictive decision. |
Operator holds and recovery
Application responses include operator_hold. While true, new applications, releases, resume, payment orders and payment initiation return HTTP 423 principal_held. Existing payment verification can finish and credit once; funding never releases a hold or resumes runtime. Contact the operator with the principal/application ID.
Authenticated inspection, configuration export in supported states and deletion remain available. A hold being accepted does not prove the host has stopped. Stop confirmation is asynchronous; commitments stay reserved until cleanup or stop is confirmed. Releasing a hold leaves applications stopped. Resume a metered application explicitly before its existing retention deadline. Legacy stopped applications retain their reservations until deletion.
During isolated database recovery, access and execution are fenced with HTTP 503 recovery_fenced. Restored credentials are invalidated; an old credential cannot recover authority through a retry.
Verified test payments
Payment orders are optional and disabled by default. An operator enables the offline fake adapter or a dedicated Stripe test account; hosting stays simulated. New bootstrap credentials include payments:read and payments:write. Existing credentials keep their original scopes; bootstrap a fresh test principal to try this flow. No real money is accepted.
order=$(curl -sS "$EUPHORIC_URL/v1/payment-orders" \
-H "Authorization: Bearer $EUPHORIC_TOKEN" \
-H 'Idempotency-Key: payment-order-1' -H 'Content-Type: application/json' \
-d '{"amount_minor":100,"currency":"EUR"}')
payment_status=$(echo "$order" | jq -r .status_url)
curl -sS "$EUPHORIC_URL$payment_status/pay" \
-H "Authorization: Bearer $EUPHORIC_TOKEN" \
-H 'Idempotency-Key: payment-start-1' -H 'Content-Type: application/json' -d '{}'
curl -sS "$EUPHORIC_URL$payment_status" \
-H "Authorization: Bearer $EUPHORIC_TOKEN"
Creation and initiation require payments:write; reads require payments:read and no idempotency header. Send integer EUR cents from 100 to 3000. Each quote expires in one hour; at most three unresolved orders and ten new orders per hour are allowed. Replays retain the original response and do not consume quota. Clients cannot set credit, provider IDs, metadata or redirect destinations. A changed body under the same key returns 409; quota errors return 429 with Retry-After, and disabled payments return 503 payment_unavailable.
Poll the order itself: created, creating, requires_payment, reconciling, credited, expired, failed or needs_review. While payable, Stripe orders expose a sensitive next_action.href Checkout URL. Open it to complete a test Checkout and keep polling. Responses are no-store; do not log Checkout URLs. Returning from Checkout has no payment effect. Only credited confirms a committed posting. For needs_review, retain the order ID and ask the operator to reconcile; do not replace an uncertain payment.
The balance reports grant_credited_micro and payment_credited_micro separately. Both are non-financial test EUR. The €100 sandbox grant cap still counts only grants. Provider API outages and duplicate notifications recover through durable work and canonical readback. Top-up does not resume applications, extend retention, raise spend ceilings or release commitments. Refunds/disputes require operator review; automatic reversals are unsupported.
Agent payments through MPP
With SPT intake explicitly enabled by the operator, create an order with payment_method: stripe_spt. Omitting the field keeps Stripe Checkout. The method is immutable. This path accepts test-only, card-backed Shared Payment Tokens in EUR; it does not accept live money, raw card details, native Link Pay Tokens or stablecoins. Real Link wallet acceptance remains under validation.
spt_order=$(curl -sS "$EUPHORIC_URL/v1/payment-orders" \
-H "Authorization: Bearer $EUPHORIC_TOKEN" \
-H 'Idempotency-Key: spt-order-1' -H 'Content-Type: application/json' \
-d '{"amount_minor":100,"currency":"EUR","payment_method":"stripe_spt"}')
spt_status=$(echo "$spt_order" | jq -r .status_url)
# Discover the stored challenge; expect HTTP 402 and WWW-Authenticate.
curl -sS -i "$EUPHORIC_URL$spt_status/pay" \
-H "Authorization: Bearer $EUPHORIC_TOKEN" \
-H 'Idempotency-Key: spt-pay-1' -H 'Content-Type: application/json' -d '{}'
# Requires Node 22 and a payer-controlled, eligible Link wallet.
npx @stripe/link-cli@0.22.0 auth login
npx @stripe/link-cli@0.22.0 mpp pay "$EUPHORIC_URL$spt_status/pay" \
--test -X POST -d '{}' \
-H "Authorization: Bearer $EUPHORIC_TOKEN" \
-H 'Idempotency-Key: spt-pay-1' \
--context 'Approve a EUR 1.00 test-only payment to Euphoric for simulated hosting credit. This is an integration test and must not charge real funds.'
# Explicit continuation: repeat this GET while pending. CLI exit 0 is not credit.
curl -sS -i "$EUPHORIC_URL$spt_status" \
-H "Authorization: Bearer $EUPHORIC_TOKEN"
curl -sS "$EUPHORIC_URL/v1/billing/balance" \
-H "Authorization: Bearer $EUPHORIC_TOKEN"
The agent follows Link's approval instructions using the original spend-request ID. The credential goes in Payment-Authorization, leaving bearer Authorization intact. Keep tokens and payment headers out of logs. The server binds the entire echoed challenge to the owner, order, route, POST, empty body, test profile, amount and expiry; encoded headers are limited to 8 KiB. Invalid, tampered, expired or ambiguous credentials fail without charging. A changed token on an accepted order or reuse across orders returns 409.
HTTP 202 means durable acceptance with payment_status: pending and next_action: poll_order; it has no receipt. The pinned Link CLI does not poll automatically. Only GET showing state: credited and a receipt, also returned in Payment-Receipt, confirms the committed test ledger credit. The receipt is stable and contains the order and canonical PaymentIntent IDs. Exact POST retries return the original 202, including retries without the payment header; do not interpret that saved response as current state.
After a timeout, retain the same order, idempotency key and Link spend-request ID. Poll instead of requesting another spend. processing at Stripe remains pending even after quote expiry. Declines, unusable tokens and unsupported additional authentication become needs_review without another charge or replacement payment; an operator can inspect and reconcile the original identity. Use the application creation or explicit resume calls below after credit; funding alone never resumes an application.
Contact verification, notices and admission
Bootstrap sends nothing. A new credential includes contact and safety scopes; an operator can explicitly upgrade one existing active credential without changing its identity. Delivery defaults to disabled. Verification proves control of a destination, not identity, trust, funding or permission to host.
Use the same EUPHORIC_TOKEN and EUPHORIC_URL as the bootstrap example. All mutations need a fresh idempotency key; after a timeout repeat the exact key and body. Requests accept only the documented fields and at most 8 KiB of JSON. Reads need no key and return private, no-store data.
curl $EUPHORIC_URL/v1/contact -H "Authorization: Bearer $EUPHORIC_TOKEN"
curl -X POST $EUPHORIC_URL/v1/contact/verifications \
-H "Authorization: Bearer $EUPHORIC_TOKEN" -H "Idempotency-Key: verify-1" \
-H 'Content-Type: application/json' -d '{}'
The 202 response contains challenge_id, expires_at, delivery_state and confirmation_path, never the code. Challenges last 30 minutes. Limits are one request/minute and five/hour/principal, plus ten/day/destination; resends invalidate the pending old challenge. Disabled or missing delivery configuration returns 503 before creating a challenge.
Email contains a fragment-based link to /contact/verify. Opening it does not verify anything: press Verify contact. Manual challenge/code entry also works without JavaScript. Callback delivery sends a signed contact.verification_requested event containing challenge_id, expires_at and token. A 2xx callback acknowledgement alone never verifies contact. The agent submits the received code:
# Set CHALLENGE and CODE from your private email or signed callback event.
curl -X POST "$EUPHORIC_URL/v1/contact/verifications/$CHALLENGE/confirm" \
-H "Authorization: Bearer $EUPHORIC_TOKEN" -H "Idempotency-Key: confirm-1" \
-H 'Content-Type: application/json' -d "{\"token\":\"$CODE\"}"
curl $EUPHORIC_URL/v1/admission -H "Authorization: Bearer $EUPHORIC_TOKEN"
curl $EUPHORIC_URL/v1/notices -H "Authorization: Bearer $EUPHORIC_TOKEN"
Callbacks require HTTPS port 443, a canonical lowercase hostname and no credentials, query or fragment. Internal/special-use or platform-management destinations, mixed unsafe DNS answers and redirects are rejected. A callback accepted by bootstrap may be ineligible for delivery: bootstrap a new principal before funding if the destination needs changing. Contact changes and credential recovery are not available.
Callback headers are Euphoric-Key-Id, Euphoric-Timestamp, Euphoric-Event-Id and Euphoric-Signature. Verify Ed25519 against the trusted origin’s /.well-known/euphoric-notification-keys.json, signing the UTF-8 bytes timestamp + "\n" + event_id + "\n" + SHA256(raw_body). The hash is lowercase hex and the signature is Base64. Reject timestamps outside five minutes, then deduplicate event IDs. Retries reuse the event ID/body, with a fresh timestamp. Never trust a key URL supplied inside a received event.
Notices contain only published customer summaries and safe delivery/hold status. Follow next_cursor with ?cursor=…; pages contain at most 50 results. Read a notice at GET /v1/notices/:id. An actionable restriction allows one appeal of 20–2,000 characters, at most five/day/principal:
# Set NOTICE to the ID of your current restrictive notice.
curl -X POST "$EUPHORIC_URL/v1/notices/$NOTICE/appeals" \
-H "Authorization: Bearer $EUPHORIC_TOKEN" -H "Idempotency-Key: appeal-1" \
-H 'Content-Type: application/json' \
-d '{"message":"We removed the affected content. Please review the restriction."}'
# Read the returned appeal_id at GET /v1/appeals/:id.
Held or unadmitted principals keep contact, notice, appeal, inspection, supported export, suspend and deletion access. A successful appeal resolves only that case’s reason; all other reasons and confirmed stops still gate explicit operator hold release. Neither appeal, payment, verification nor hold release resumes applications or grants admission.
When cohort policy applies, create/release/resume and runtime authorization require verified contact and explicit operator approval. Membership policy persists after the deployment flag is disabled. Applications reserve cohort capacity until confirmed cleanup. Errors include 403 contact_unverified/alpha_not_admitted, 409 alpha_capacity/state_conflict, 423 principal_held, 429 rate_limited with Retry-After, and 503 recovery_fenced. Funding grants no admission.
Anyone can report an application without a credential. JSON clients POST /abuse/reports with a client-generated 64-character lowercase hex Idempotency-Key, application_id or application_url, a reason (phishing, malware, spam, harmful_content, other), and a 20–4,000 character description. Optional reporter_email is at most 254 characters; evidence_urls holds at most three 2,048-character strings. The entire body is at most 16 KiB. Send no cookies or browser Origin with JSON; HTML forms require CSRF. Limits are five/hour/actual peer and 100/hour globally. The generic 202 receipt never reveals whether a target exists. Reports do not create holds, fetch evidence, or email reporters.
Legacy fixed-reservation credit
Older clients use a separate allowance instead of the metered plan. New clients should use the metered path above.
curl -sS -X POST "$EUPHORIC_URL/v1/sandbox/credits" \ -H "Authorization: Bearer $EUPHORIC_TOKEN" \ -H 'Idempotency-Key: credit-1'
This legacy path grants €30 once per principal. Creating an app without plan_id reserves €1; deletion releases it. This allowance is separate from the metered test ledger and future real funding.
To check your balance without changing it, use GET /v1/balance. Amounts are EUR minor units (cents): credited_minor, reserved_minor, and available_minor.
curl -sS "$EUPHORIC_URL/v1/balance" \ -H "Authorization: Bearer $EUPHORIC_TOKEN"
Private staging integration
In staging, DELETE /v1/applications/:id queues route withdrawal and tenant cleanup. Poll the returned operation until it succeeds. Capacity and test-credit reservations remain held until cleanup is confirmed; a failed cleanup keeps them reserved. The hostname remains reserved after deletion.
The private staging integration selects provider: hetzner only for explicitly configured, operator-admitted staging principals. It is separate from this browser sandbox. Responses identify environment: sandbox|staging, billing_mode: test, and sandbox: true only for simulated execution. Default production exposes neither mode.
Staging credit is granted by an operator; /v1/sandbox/credits remains disabled there. Applications require a curated digest-pinned fixture, a free campaign slot, and a separate provider spending reservation. Admission and exhausted budgets produce actionable errors before allocation. Staging is private, and no payment or anonymous hosting is enabled.
Staging URLs use https://<slug>.euphoric-app.eu and appear only after readiness and trusted HTTPS verification. The returned staging slug already includes its stg- prefix. Read the returned URL instead of constructing it. A healthy replacement keeps this URL; failed candidates retain the previous release. Runtime logs use source: container, identify the release and stdout/stderr stream, and return up to 100 lines / 64 KiB. Pass the returned opaque cursor as ?cursor=...; its timestamp is inclusive, so the last boundary line may repeat. A cursor expires after one hour and cannot be reused for another release. Retry without a cursor on runtime_logs_unavailable (503). Operation events remain in the operation response.
Exports contain application configuration and the active digest. They exclude images, databases, persistent files, credentials, certificates, and management details. Controlled staging HTTPS journeys, recovery checks and a 30-boot benchmark passed. These checks do not certify production hosting.
What comes next
Server provisioning, Docker execution, TLS routing, metered funding, suspension and recovery have been verified for private, known-image staging; the temporary acceptance infrastructure has been removed. Real payments, public hosting, source builds, databases, workers, custom domains and human claim are future work. The development sandbox must not be exposed as a production hosting service.