Endpoint Setup
This guide walks you through the different authentication methods and how to use them along with the spec reference and sample code to help you write your translation endpoints logic and run some basic testing.
The five endpoints at a glance
The table below lists all five endpoints. The first four are listed in the order Adobe Express calls them during a translation session. /health is not part of the runtime flow and is listed separately.
We also recommend checking out the starter project for an example implementation of all five endpoints.
GET/localesGET/tones/localesPOST/translatePOST/feedbackGET/healthThe first thing you should do before building your translation connector service, is choose an authentication method. Adobe Express Translate Connectors support four authentication options: OAuth 2.0 PKCE, Secure API Key, plain API Key (development and testing only), and no authentication. Continue below to understand each option, what to configure in your service, and what you can test locally right now, versus what requires the Adobe Express Connector Playground access to test end-to-end.
Choose your authentication type
The auth type you select applies uniformly to every endpoint in your connector. You cannot mix types (for example, API Key for /locales and /tones with OAuth 2.0 PKCE for /translate). Endpoints that should include credentials are marked with useAuth: true in the apiConfig; see the Manifest Schema Reference for the field-level details.
data-slots=heading, text
data-variant=warning
Option 1: OAuth 2.0 PKCE
OAuth 2.0 PKCE (Proof Key for Code Exchange) gives each user their own authorization session. When a user connects to your service from the Connector Playground or the production Translate panel, Adobe Express opens a popup to the authorizationUrl defined in your manifest. After the user completes the flow, Adobe Express exchanges the authorization code for an access token using the tokenUrl from your configured manifest and includes that token in every subsequent API call.
Register the redirect URIs
When registering your OAuth application with your authorization server (Auth0, Okta, Azure AD, or any other provider), you must allowlist the redirect URIs that Adobe Express uses after the user completes authorization. Add both of the following:
https://express.adobe.com/static/oauth-redirect.html
https://new.express.adobe.com/static/oauth-redirect.html
Adobe Express is in the process of migrating from new.express.adobe.com to express.adobe.com. If you allowlist only one, OAuth will fail for users on the other domain. Register both now, even if you're not ready to test the full flow yet.
OAuth popup flow
When a user clicks Connect in the Connector Playground (or selects your service in the production Translate panel for the first time), Adobe Express opens a popup window to the authorizationUrl from your manifest. The user signs in on your authorization server, grants the scopes you've requested, and the popup closes automatically once authorization completes. Adobe Express then exchanges the authorization code for an access token using your tokenUrl and stores the token for use on subsequent API calls.
The login form shown in the popup is rendered by your authorization server, not by Adobe Express. The exact layout, branding, and fields will match whatever your provider serves at the authorizationUrl.
After the popup closes, the Playground shows a green "successfully connected" toast and the connector becomes available in the Translate panel. If you see a red error toast instead, or the popup shows a provider error page that does not close, see Troubleshooting: Authentication errors.
PKCE code challenge method
Adobe Express uses code_challenge_method=S256 (SHA-256) when generating the PKCE code challenge. Your authorization server must support the S256 method. Most major providers (Auth0, Okta, Azure AD, Google) support S256 by default, but verify your configuration - some servers require it to be explicitly enabled or set as the required method. The plain challenge method is not used and should not be accepted as a fallback.
How Adobe Express sends the access token
After a successful token exchange, Adobe Express sends the access token in the Authorization header of every API call to endpoints where you've configured authentication to apply when you build your manifest.
Authorization: Bearer <access_token>
Token validation sample
Your service must validate this token on every protected request. The following example is written for the TypeScript starter project (translate-connector-standalone), but the same pattern applies in any language:
import express, { Request, Response, NextFunction } from "express";
function requireBearerToken(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers["authorization"];
if (!authHeader || !authHeader.startsWith("Bearer ")) {
res.status(401).json({ errorCode: "Unauthorized", errorMessage: "Missing or invalid Authorization header" });
return;
}
const token = authHeader.slice("Bearer ".length);
// Replace with your authorization server's token introspection
// endpoint or local JWT verification logic.
if (!isValidToken(token)) {
res.status(401).json({ errorCode: "Unauthorized", errorMessage: "Token validation failed" });
return;
}
next();
}
// Apply to protected routes. /health typically does not require auth.
app.get("/locales", requireBearerToken, (req, res) => { /* ... */ });
app.get("/tones", requireBearerToken, (req, res) => { /* ... */ });
app.post("/translate", requireBearerToken, (req, res) => { /* ... */ });
app.post("/feedback", requireBearerToken, (req, res) => { /* ... */ });
Token refresh: Adobe Express automatically refreshes expired access tokens using the refresh token from the initial exchange. No code is required on your end. To ensure this works, confirm that your authorization server issues a refresh token. Some providers require specific scopes (for example, offline_access for Auth0) or explicit configuration to enable refresh token issuance. If a refresh token is not returned, users will be prompted to re-authorize when their access token expires.
Testing token validation locally
You can test this today without the Playground by issuing a curl request with a token:
# Should return 401 with errorCode "Unauthorized" - no Authorization header
curl -X POST http://localhost:8787/translate \
-H "Content-Type: application/json" \
-d '{"sourceLocale":"en-US","targetLocale":"fr-FR","items":["Hello"]}'
# Should return 401 with errorCode "Unauthorized" - token fails validation
curl -X POST http://localhost:8787/translate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer invalid-token" \
-d '{"sourceLocale":"en-US","targetLocale":"fr-FR","items":["Hello"]}'
# Should return 200 with translation result
curl -X POST http://localhost:8787/translate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your-test-token>" \
-d '{"sourceLocale":"en-US","targetLocale":"fr-FR","items":["Hello"]}'
The full user-facing popup flow (where Adobe Express opens your authorizationUrl, the user logs in, and tokens are exchanged) requires the Connector Playground.
Option 2: Plain API Key (development and testing only)
Use plain API Key authentication during development and end-to-end testing in the Connector Playground when standing up OAuth would slow you down. You store the key in authConfig.apiKey and use the $apiKey placeholder to specify exactly where Adobe Express injects it: in a header, query param, or body field in each endpoint's apiConfig.
data-slots=heading, text
data-variant=warning
Manifest configuration
Your manifest stores the key in authConfig.apiKey. To send it with a request, add the $apiKey placeholder as the value for a header in apiConfig.headers. Adobe Express resolves $apiKey to the stored key value before making the request:
"authConfig": { "type": "API_KEY", "apiKey": "your-api-key" },
"apiConfig": [
{
"id": "translate",
"endpoint": "https://your-service.example/translate",
"method": "POST",
"headers": { "x-api-key": "$apiKey" },
"useAuth": true
}
]
The header name (x-api-key in this example) is your choice. $apiKey resolves to the entire header value. It cannot be embedded within a string (for example, "Bearer $apiKey" does not work; the placeholder must be the complete value). See Manifest Schema Reference for field details.
API key validation sample
const EXPECTED_API_KEY = process.env.CONNECTOR_API_KEY ?? "your-api-key";
function requireApiKey(req: Request, res: Response, next: NextFunction) {
const key = req.headers["x-api-key"];
if (!key || key !== EXPECTED_API_KEY) {
res.status(401).json({ errorCode: "Unauthorized", errorMessage: "Missing or invalid API key" });
return;
}
next();
}
app.get("/locales", requireApiKey, (req, res) => { /* ... */ });
app.get("/tones", requireApiKey, (req, res) => { /* ... */ });
app.post("/translate", requireApiKey, (req, res) => { /* ... */ });
app.post("/feedback", requireApiKey, (req, res) => { /* ... */ });
Replace "x-api-key" with the header name you configured in apiConfig.headers.
Test locally
# Should return 401 - missing key header
curl http://localhost:8787/locales
# Should return 401 - wrong key
curl http://localhost:8787/locales \
-H "x-api-key: wrong-key"
# Should return 200 with the locales list
curl http://localhost:8787/locales \
-H "x-api-key: your-api-key"
A good practice is to store CONNECTOR_API_KEY in an environment variable rather than hardcoding it.
Option 3: Secure API Key
Secure API Key is the production-supported static-credential authentication type. Your manifest declares the auth type and uses a placeholder in your endpoint headers. Adobe stores your key server-side and injects it into the request at runtime before forwarding to your service. The key never appears in your manifest, never reaches client devices, and exists only for the lifetime of the forwarded request.
How it works:
- Adobe Express receives the translation request and routes it through Adobe's secure bridge.
- The bridge resolves the registered key for the user's Adobe org.
- The bridge populates the header you specified (for example,
x-api-key) and forwards the request to your endpoint. - Your service receives a normal HTTP request with the header populated.
Note: Only Secure API Key connectors are routed through Adobe's bridge. OAuth 2.0 PKCE and plain API Key connectors call your service directly.
Manifest configuration
Set authConfig.type to "SECURE_API_KEY" and add the $secureApiKey placeholder as the value for your header in apiConfig.headers. Set useAuth: true on any endpoint that authenticates against your service, to make the intent explicit in the manifest.
"authConfig": { "type": "SECURE_API_KEY" },
"apiConfig": [
{
"id": "translate",
"endpoint": "https://your-service.example/translate",
"method": "POST",
"headers": { "x-api-key": "$secureApiKey" },
"useAuth": true
}
]
The header name (x-api-key in this example) is your choice. It must match the header your backend reads for validation.
Backend validation
Validate the header value the same way you would for any static key. Replace "x-api-key" with the header name you configured in apiConfig.headers:
import express, { Request, Response, NextFunction } from "express";
const EXPECTED_API_KEY = process.env.CONNECTOR_API_KEY ?? "your-registered-key";
function requireApiKey(req: Request, res: Response, next: NextFunction) {
const key = req.headers["x-api-key"];
if (!key || key !== EXPECTED_API_KEY) {
res.status(401).json({ errorCode: "Unauthorized", errorMessage: "Invalid or missing API key" });
return;
}
next();
}
app.post("/translate", requireApiKey, (req, res) => { /* ... */ });
Register your key with Adobe
Configuring authConfig and validating the header in your service isn't enough on its own. You must also add your key as a credential from your connector's Settings tab so Adobe's secure bridge knows how to route requests for your connector. A connector has a single Secure API Key credential that applies globally, no matter which listing type you use to distribute it: private share link, internal listing, or public listing.
data-slots=heading, text1, text2
data-variant=warning
Test locally
Test your endpoint validation before adding your key as a credential by sending the header directly:
# Should return 401 - missing key header
curl -X POST http://localhost:8787/translate \
-H "Content-Type: application/json" \
-d '{"sourceLocale":"en-US","targetLocale":"fr-FR","items":["Hello"]}'
# Should return 401 - wrong key
curl -X POST http://localhost:8787/translate \
-H "Content-Type: application/json" \
-H "x-api-key: wrong-key" \
-d '{"sourceLocale":"en-US","targetLocale":"fr-FR","items":["Hello"]}'
# Should return 200 with translation result
curl -X POST http://localhost:8787/translate \
-H "Content-Type: application/json" \
-H "x-api-key: your-registered-key" \
-d '{"sourceLocale":"en-US","targetLocale":"fr-FR","items":["Hello"]}'
End-to-end validation, where Adobe's bridge injects the key automatically, requires the Connector Playground after you've added your credential.
Migrate from plain API Key
To migrate an existing plain API Key connector to Secure API Key:
- Change
authConfig.typefrom"API_KEY"to"SECURE_API_KEY". - Remove the
apiKeyfield fromauthConfig. - Add a
headersobject to eachapiConfigendpoint whereuseAuth: true, using"$secureApiKey"as the value for your chosen header name (for example,"headers": { "x-api-key": "$secureApiKey" }). - Update your backend to validate the key from the header you chose (for example,
req.headers["x-api-key"]). - Follow the steps in Register your Secure API Key with Adobe to add your key as a credential from your connector's Settings tab.
Your backend validation logic only needs to change if the header name differs between your $apiKey and $secureApiKey configurations.
Option 4: No authentication
Use no authentication when:
- Your service is intentionally designed to be open to the internet (for example, a publicly accessible translation service that does not require per-user or per-org credentials)
- You're running your service locally for development only
- Your service uses a custom authorization mechanism
HTTPS Requirements
Your connector endpoints must be served over HTTPS with a valid TLS certificate for Adobe Express to connect to them reliably, both for end-to-end testing in the Connector Playground and once your connector is deployed.
- OAuth 2.0 PKCE: Both
authorizationUrlandtokenUrlmust use HTTPS. The manifest validator will reject HTTP values for these fields. - API Key and no auth: HTTPS is required for production.
- The
Authorizationheader is not protected over plain HTTP. Credentials sent over HTTP can be intercepted. Plan your HTTPS hosting setup before deploying to a shared or production environment.
Implementing the endpoints
Once you have chosen your authentication method, implement the five HTTP endpoints. The Translate Connector API Reference defines the complete contract including request and response schemas, field-level requirements, and behavioral rules. The response shapes below use the TypeScript types from translate-connector-sdk.d.ts, included in the starter project and available as a standalone download from Getting Started.
Use the starter project as a scaffold or generate stubs from the OpenAPI YAML in your preferred language.
GET /locales
Returns the list of locales your service supports. Adobe Express calls this when the Translate panel opens to populate the target language dropdown. If omitted, Adobe Express falls back to its built-in locale list.
import type { LocalesResponse } from "@adobe-ccwebext/ccweb-connector-sdk-types/translate";
// 200 OK
const response: LocalesResponse = {
locales: [
{ code: "en-US", label: "English (US)", category: "Popular languages" },
{ code: "fr-FR", label: "French (France)", category: "Popular languages" },
{ code: "ja-JP", label: "Japanese", category: "Other languages" }
]
};
The category field is optional. Use it to group locales in the picker. Adobe Express passes a preferredLanguage query parameter based on the user's Adobe Express language setting. Use it to return locale labels in the user's preferred language.
GET /tones
Returns the list of tones your service supports. Adobe Express calls this when the Translate panel opens, in parallel with /locales. Only called if your manifest declares a tones API endpoint. Omit this endpoint and the tones entry in apiConfig if your service does not support tones.
import type { TonesResponse } from "@adobe-ccwebext/ccweb-connector-sdk-types/translate";
// 200 OK
const response: TonesResponse = {
tones: [
{ value: "Formal", label: "Formal" },
{ value: "Informal", label: "Informal" },
{ value: "Professional", label: "Professional" }
]
};
POST /translate
Translates an array of text items. This is the only required endpoint. Adobe Express sends a TranslationRequest when the user triggers a translation.
Request:
import type { TranslationRequest } from "@adobe-ccwebext/ccweb-connector-sdk-types/translate";
const request: TranslationRequest = {
sourceLocale: "en-US",
targetLocale: "fr-FR",
items: ["Hello, world!", "Click to edit this text."],
tone: "Formal" // optional
};
Response:
import type { TranslationResponse } from "@adobe-ccwebext/ccweb-connector-sdk-types/translate";
// 200 OK - result array must match the length and order of request.items
const response: TranslationResponse = {
result: ["Bonjour, le monde!", "Cliquez pour modifier ce texte."]
};
Error response:
const errorResponse: TranslationResponse = {
result: [],
errorCode: "UnsupportedLocale",
errorMessage: "The target locale fr-FR is not supported by this service."
};
For all supported translate-specific error codes, see Translate-Specific Error Codes in the Translate Connector API Reference.
POST /feedback
Receives feedback submitted by users after a translation. Adobe Express calls this when the user submits a thumbs up or thumbs down rating. Optional but recommended.
Request:
import type { FeedbackRequest } from "@adobe-ccwebext/ccweb-connector-sdk-types/translate";
const request: FeedbackRequest = {
type: "Negative",
reason: "TranslationError",
note: "The word 'world' was translated incorrectly." // optional
};
Response:
import type { FeedbackResponse } from "@adobe-ccwebext/ccweb-connector-sdk-types/translate";
// 200 OK
const response: FeedbackResponse = {};
Feedback type values:
typePositiveNegativeNegative feedback reason values:
reasonTranslationErrorIncorrectToneIncorrectLayoutLongLoadTimeHarmfulOrBiasContentCopyrightTrademarkViolationNudityOrSexualContentViolenceOrGoreOtherPositive feedback reason values:
reasonAccurateTranslationCorrectTonePreservedLayoutQuickLoadImpressiveOtherThe optional note field contains free-form text entered by the user. Forward these to your analytics or quality pipeline to improve translation quality over time.
GET /health
Returns the availability status of your service. Only called by the Connector Playground endpoint tester during setup. Not called during normal Adobe Express runtime.
import type { HealthResponse } from "@adobe-ccwebext/ccweb-connector-sdk-types/translate";
// 200 OK
const response: HealthResponse = {
message: "OK"
};
On error, include errorCode and errorMessage:
const errorResponse: HealthResponse = {
errorCode: "GenericError",
errorMessage: "Service temporarily unavailable"
};
Error codes
All five connector response types include optional errorCode and errorMessage fields. The TypeScript SDK type definitions (translate-connector-sdk.d.ts) define the valid errorCode string values.
For authentication failures, return HTTP 401 with errorCode: "Unauthorized". Refer to the sample code in each authentication option above for the exact pattern. For malformed requests, return HTTP 400 with errorCode: "BadRequest". For unexpected server-side errors, return HTTP 500 with errorCode: "GenericError".
{
"errorCode": "Unauthorized",
"errorMessage": "Missing or invalid Authorization header"
}
For the complete list of error codes including the /translate-specific codes that map to user-facing messages in Adobe Express, see Error Codes in the Translate Connector API Reference.
Testing your service
Testing with curl
A quick way to verify your service is behaving correctly and returning the proper status codes before the Connector Playground is available, is to call each endpoint directly with curl. The examples below use the default port from the starter project:
# Verify your service is reachable
curl http://localhost:8787/health
# Check your supported locales
curl http://localhost:8787/locales
# Check your supported tones
curl http://localhost:8787/tones
# Translate some text
curl -X POST http://localhost:8787/translate \
-H "Content-Type: application/json" \
-d '{
"sourceLocale": "en-US",
"targetLocale": "fr-FR",
"items": ["Hello, world!", "Welcome to Adobe Express."],
"tone": "formal"
}'
# Submit positive feedback
curl -X POST http://localhost:8787/feedback \
-H "Content-Type: application/json" \
-d '{ "type": "Positive", "reason": "AccurateTranslation" }'
Testing authentication
OAuth 2.0 PKCE and plain API Key send credentials differently, so test each using the header format for your auth type. The per-auth-type curl samples above cover both cases. For a quick reference:
OAuth 2.0 PKCE uses Authorization: Bearer <token>:
# Should return 401 - missing Authorization header
curl -X POST http://localhost:8787/translate \
-H "Content-Type: application/json" \
-d '{"sourceLocale":"en-US","targetLocale":"fr-FR","items":["Hello"]}'
# Should return 401 - invalid token
curl -X POST http://localhost:8787/translate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer wrong-token" \
-d '{"sourceLocale":"en-US","targetLocale":"fr-FR","items":["Hello"]}'
# Should return 200
curl -X POST http://localhost:8787/translate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-valid-token" \
-d '{"sourceLocale":"en-US","targetLocale":"fr-FR","items":["Hello"]}'
Plain API Key uses whatever header you configured with $apiKey in your manifest (for example, x-api-key):
# Should return 401 - missing key header
curl -X POST http://localhost:8787/translate \
-H "Content-Type: application/json" \
-d '{"sourceLocale":"en-US","targetLocale":"fr-FR","items":["Hello"]}'
# Should return 401 - wrong key
curl -X POST http://localhost:8787/translate \
-H "Content-Type: application/json" \
-H "x-api-key: wrong-key" \
-d '{"sourceLocale":"en-US","targetLocale":"fr-FR","items":["Hello"]}'
# Should return 200
curl -X POST http://localhost:8787/translate \
-H "Content-Type: application/json" \
-H "x-api-key: your-api-key" \
-d '{"sourceLocale":"en-US","targetLocale":"fr-FR","items":["Hello"]}'
All three responses should behave as you expect before you connect the Playground. Once you have verified your service is behaving correctly, load the connector in the Playground and run a complete end-to-end test. See Connector Playground for access and step-by-step instructions, and Test Your Service for the full pre-Playground and end-to-end testing checklist.
Enable CORS for Playground testing
curl is not affected by CORS. It is a browser-only restriction. Once you move from curl to the Connector Playground, Adobe Express makes requests from the browser (express.adobe.com) to your local service. Browsers block these cross-origin requests unless your service returns an Access-Control-Allow-Origin header.
The TypeScript starter project already handles this via the cors middleware:
import cors from "cors";
app.use(cors()); // allow all origins for local development
If you are building your service in a different language or framework, add equivalent CORS support before connecting through the Playground. For example:
# Python / Flask
from flask_cors import CORS
CORS(app)
// Spring Boot
@CrossOrigin(origins = "*")
data-slots=heading, text
data-variant=warning
*) is only appropriate for local development. Before deploying your service, restrict the Access-Control-Allow-Origin header to the Adobe Express domains: https://express.adobe.com and https://new.express.adobe.com.Best practices
Implement a /health endpoint
The /health endpoint is used by the Connector Playground endpoint tester during connector setup to verify your service is reachable. It is not called during normal Adobe Express runtime. Return HTTP 200 when your service is available. When your service is degraded or unavailable, return an appropriate HTTP error status or include errorCode in the response body to signal the condition.
import type { HealthResponse } from "@adobe-ccwebext/ccweb-connector-sdk-types/translate";
// Healthy
const healthy: HealthResponse = {
message: "OK"
};
// Degraded - include errorCode to describe the condition
const degraded: HealthResponse = {
errorCode: "GenericError",
errorMessage: "Upstream translation service is temporarily unavailable"
};
Test it with curl:
# Expected: HTTP 200 with {"message":"OK"} or similar
curl -v http://localhost:8787/health
Use standard HTTP status codes for authentication and request errors
For all non-health endpoints, return the appropriate HTTP status code alongside the error body. The connector API uses a fixed set of errorCode values that map to specific behaviors in Adobe Express:
UnauthorizedBadRequestGenericError, UnsupportedLocale, etc./translate endpointReturn 401 for authentication failures so that Adobe Express and the Playground can surface actionable errors to developers. Return 400 for request validation errors. For translation-specific application errors such as UnsupportedLocale or ServiceCapacity, include errorCode in the response body - Adobe Express reads this field to display the appropriate message to the user.
// 401 - authentication failure (any protected endpoint)
res.status(401).json({
errorCode: "Unauthorized",
errorMessage: "Missing or invalid Authorization header"
});
// 400 - malformed request
res.status(400).json({
errorCode: "BadRequest",
errorMessage: "Request body is missing required field: targetLocale"
});
// 200 - translate application error (translate endpoint only)
res.status(200).json({
result: [],
errorCode: "UnsupportedLocale",
errorMessage: "The locale zh-TW is not supported by this service"
});
Cache /locales and /tones responses
Adobe Express calls /locales and /tones each time the Translate panel opens, before the user submits a translation. If these endpoints are slow, users see a loading state before the panel becomes usable. If your locale and tone lists are mostly static, cache the responses in memory on your service and return them immediately.
Store secrets in environment variables
Never hardcode API keys or OAuth client secrets in your source files. Store them in environment variables and read them at startup. For API Key auth, the key is defined in your manifest, but your service-side validation logic should read the expected key from an environment variable so it can be rotated without a code change.
Related resources
- translate-connector-api.yaml: The authoritative OpenAPI 3.0 specification. Download from Getting Started.
- translate-connector-sdk.d.ts: TypeScript type definitions for all request and response payloads. Download from Getting Started.
- translate-connector-standalone.zip: Self-contained Node.js starter project with all five endpoints implemented as stubs, plus the SDK types bundled inside. Download from Getting Started.
- Manifest Schema Reference: Annotated details about the manifest used by Adobe Express when connecting to your service. Note: you do not need to build a manifest now. The information is provided to help you understand how your curl testing matches the relevant manifest sections defined with auth.
- Test Your Service: Recommended testing order, per-endpoint
curlverification, Playground end-to-end checks, and error code reference.