Generate with Visual Effect
Examples of Generation with Visual Effect
Image Editing

Input Image

Output Image with "Lofi Style" visual effect
Video Generation

Input Image
NOTE:
- The maximum allowed size for input image is 5 MB.
Request
The API requires both a effect_id and an input image to work, with the input image serving as the base that will be processed according to prompt defined by the Visual Effect.
https://open.eternalai.org/generate
curl --request POST \
--location 'https://open.eternalai.org/generate' \
--header 'x-api-key: <YOUR_API_KEY>' \
--header 'Content-Type: application/json' \
--data '{
"images": [
"https://cdn.eternalai.org/feed/2025/12/29/5ba974c9-d0c1-44c9-a65b-720bec126615.jpg"
],
"effect_id": "574212263f3107352b0d07"
}'
| Parameter | Type | Required | Description |
|---|---|---|---|
images | array | Yes | A image URL or Base64-encoded image (≤ 5 MB) used as input for the Transform Visual Effect process. |
effect_id | string | Yes | The unique identifier of the Visual Effect to apply. The |
duration | int | No | Duration of the video effect in seconds. Only used for video effects. |
audio | boolean | No | Only used for video effects. |
Input Image Format
The images field in the request accepts both formats: URL and base64 content for these image formats: .jpg, .jpeg, .png, and .webp
HTTP/HTTPS URL: A direct link to an image.
{
"images": [
"https://cdn.eternalai.org/effects-19-11/images/Lace_lingerie/input.jpg"
]
}
Base64 Encoded Image: A data URI format string (e.g., data:image/png;base64,...).
{
"images": [
"data:image/png;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAQDAwQDAwQEAwFEZ//Z"
]
}
Response example
{
"request_id": "aae618cd-875e-4f7a-adf3-a02906ec2e81",
"status": "pending",
"result": "",
"progress": 0
}
| Parameter | Type | Description |
|---|---|---|
request_id | string | Unique identifier of the request to get result |
status | string | Current status of the request (e.g. pending, success). |
result | string | Result data returned after processing. |
progress | integer | Processing progress percentage. |
Poll for Result
Periodically, call the following result retrieval api with the request_id obtained from the previous step to poll for that request's result.
https://open.eternalai.org/poll-result
Result retrieval example:
curl --location --request GET 'https://open.eternalai.org/poll-result/$REQUEST_ID'
On success, the response is a JSON object with a result object containing a result_url field. This result_url is a signed URL for retrieving the generated image or video.
{
"request_id": "1d20f5f4-9935-40c5-b261-214875089395",
"status": "success",
"progress": 99,
"created_at": "2025-12-17T09:05:44.444Z",
"updated_at": "2025-12-17T09:06:29.03Z",
"result_url": "https://cdn.eternalai.org/agents/temp_image_1d20f5f4-9935-40c5-b261-214875089395_1765962388.jpg",
"log": "image gen: [IMAGE_WEBHOOK] - RequestID: 1d20f5f4-9935-40c5-b261-214875089395, body: {\"request_id\":\"1d20f5f4-9935-40c5-b261-214875089395\",\"cdn_url\":\"https://cdn.eternalai.org/agents/temp_image_1d20f5f4-9935-40c5-b261-214875089395_1765962388.jpg\",\"status\":{\"status\":\"completed\",\"progress\":0,\"queue_position\":null,\"total_queue_size\":null,\"started_at\":null,\"estimated_wait_time\":null,\"error\":\"\"}}",
"effect_type": "image"
}
| Parameter | Type | Description |
|---|---|---|
request_id | string | Unique identifier for the generation request. |
status | string | Final status of the request (e.g. success, pending, failed). |
progress | integer | Progress indicator of the generation process. |
created_at | string (ISO 8601) | Timestamp when the request was created. |
updated_at | string (ISO 8601) | Timestamp of the latest status update. |
result_url | string | URL of the generated output image or video. |
log | string | Internal processing log and webhook information. |
effect_type | string | Type of generation performed (e.g. image, video). |
Privacy
This section guides developers on how to integrate the API with privacy protection enabled, without storing raw input or output data on the server.
The approach is based on asymmetric encryption (RSA) combined with per-request symmetric keys.
Overview
When privacy mode is enabled:
- You share only your public key with the API.
- The server never stores plaintext input or output.
- All sensitive data is encrypted before storage or delivery.
- Only the user holding the private key can decrypt results.
This design ensures end-to-end confidentiality between you and the API, while ensuring that your data is neither stored nor reused for any other purposes.
Flow
- User generates an RSA key pair: Public Key / Private Key
- User sends the public key with the API request.
- Server:
- Generates a one-time-use symmetric key.
- Encrypts the output with that symmetric key.
- Encrypts the symmetric key using the user’s public key.
- Deletes the symmetric key after the encryption process completes
- Server returns only encrypted output + encrypted symmetric key.
- User decrypts everything locally using their private key.
- The user uses their private key to decrypt the encrypted symmetric key and obtain the symmetric key.
- Then uses that symmetric key to decrypt the output locally.
Creating an RSA Key Pair (If You Don’t Have One)
If you do not already have an RSA key pair, You can generate an RSA 2048-bit key pair locally using any standard cryptographic tool.
This ensures your private key never leaves your machine.
Generate RSA 2048-bit Key Pair
- Bash
- Python
openssl genrsa 2048 | tee private_key.pem | openssl rsa -pubout > public_key.pem
# pip install cryptography
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
# Constants
RSA_KEY_SIZE = 2048
RSA_PUBLIC_EXPONENT = 65537
def generate_rsa_key_pair() -> tuple[str, str]:
"""
Generate RSA 2048-bit key pair.
Returns:
tuple[str, str]: (public_key_pem, private_key_pem)
- public_key_pem: PKIX/SubjectPublicKeyInfo format (BEGIN PUBLIC KEY)
- private_key_pem: PKCS#1 format (BEGIN RSA PRIVATE KEY)
"""
private_key = rsa.generate_private_key(
public_exponent=RSA_PUBLIC_EXPONENT,
key_size=RSA_KEY_SIZE,
backend=default_backend(),
)
# Serialize private key in PKCS#1 format (matching Go's x509.MarshalPKCS1PrivateKey)
private_key_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL, # PKCS#1
encryption_algorithm=serialization.NoEncryption(),
).decode("utf-8")
# Serialize public key in PKIX/SubjectPublicKeyInfo format (matching Go's x509.MarshalPKIXPublicKey)
public_key_pem = (
private_key.public_key()
.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
.decode("utf-8")
)
return public_key_pem, private_key_pem
public_key_pem, private_key_pem = generate_rsa_key_pair()
public_key_pem, private_key_pem
Note
- A single RSA key pair can be reused across multiple API requests.
You do not need to generate a new key for every request unless you require key rotation.- If the private key is lost, the encrypted results can never be decrypted or recovered.
The server does not store plaintext data or private keys, so recovery is impossible.
API Request Example (Privacy Enabled)
Include your RSA public key (PKIX format) in the request body to activate privacy mode.
Request
- Bash
- Python
curl --request POST \
--location 'https://open.eternalai.org/generate' \
--header 'x-api-key: <YOUR_API_KEY>' \
--header 'Content-Type: application/json' \
--data '{
"images": [
"https://cdn.eternalai.org/feed/2025/12/29/5ba974c9-d0c1-44c9-a65b-720bec126615.jpg"
],
"effect_id": "5742102b3c0e5908200668",
"rsa_pub": "-----BEGIN PUBLIC KEY-----\n<YOUR_RSA_PUBLIC_KEY>\n-----END PUBLIC KEY-----"
}'
import requests
import json
url = "https://open.eternalai.org/generate"
headers = {
"x-api-key": "<YOUR_API_KEY>",
"Content-Type": "application/json",
}
payload = {
"images": [
"https://cdn.eternalai.org/effects-19-11/images/Lace_lingerie/input.jpg"
],
"effect_id": "291506174e2d0531060c2d1f0c",
"rsa_pub": (
"-----BEGIN PUBLIC KEY-----\n"
"<YOUR_RSA_PUBLIC_KEY>\n"
"-----END PUBLIC KEY-----"
),
# "rsa_pub": public_key_pem,
}
async_response = requests.post(
url,
headers=headers,
data=json.dumps(payload),
timeout=60,
)
# Check async_response
async_response.raise_for_status()
async_response_info = async_response.json()
print(json.dumps(async_response_info, indent=2))
| Parameter | Type | Required | Description |
|---|---|---|---|
rsa_pub | string | Optional (but recommended) | RSA public key in PEM format. The server uses this key to encrypt the AES key, which is used to encrypt the generated result |
Response example
{
"request_id": "a4a43be6-69ed-4203-a6d9-84e8aee33cbf",
"status": "pending",
"result": "",
"progress": 0
}
Poll for Result
Periodically, call the following result retrieval api with the request_id obtained from the previous step to poll for that request's result.
- Bash
- Python
REQUEST_ID="<REQUEST_ID>"
URL="https://open.eternalai.org/poll-result/${REQUEST_ID}"
POLL_INTERVAL=3 # seconds
TIMEOUT=300 # seconds
start_time=$(date +%s)
while True:
response = requests.get(url, timeout=30)
response.raise_for_status()
encrypted_result = response.json()
print(response.raise_for_status())
status = encrypted_result.get("status")
progress = encrypted_result.get("progress", 0)
if status in ("success", "failed"):
print(f"Final status: {status}")
print(json.dumps(encrypted_result, indent=2))
break
time.sleep(4)
if time.time() - start_time > TIMEOUT:
raise TimeoutError("Polling timed out")
print(f"Status: {status}, progress: {progress}% → retrying...")
time.sleep(POLL_INTERVAL)
import requests
import time
import json
REQUEST_ID = "<REQUEST_ID>"
# REQUEST_ID = async_response_info["request_id"]
url = f"https://open.eternalai.org/poll-result/{REQUEST_ID}"
POLL_INTERVAL = 3 # seconds
TIMEOUT = 300 # seconds
start_time = time.time()
while True:
response = requests.get(url, timeout=30)
response.raise_for_status()
result = response.json()
print(json.dumps(result, indent=2))
status = result.get("status")
progress = result.get("progress", 0)
if status in ("success", "failed"):
print(f"Final status: {status}")
break
if time.time() - start_time > TIMEOUT:
raise TimeoutError("Polling timed out")
print(f"Status: {status}, progress: {progress}% → retrying...")
time.sleep(POLL_INTERVAL)
On success, the API returns a JSON object containing only encrypted artifacts.
Instead of returning a direct image or video file, the response includes a result_url that points to an encrypted result stored on the server.
{
"request_id": "a4a43be6-69ed-4203-a6d9-84e8aee33cbf",
"status": "success",
"progress": 99,
"created_at": "2025-12-23T10:39:20.36Z",
"updated_at": "2025-12-23T10:40:15.382Z",
"result_url": "https://cdn.eternalai.org/encrypted-result%2Fa4a43be6-69ed-4203-a6d9-84e8aee33cbf-1766486415.jpg.encrypted",
"log": "image gen: [IMAGE_WEBHOOK] - RequestID: a4a43be6-69ed-4203-a6d9-84e8aee33cbf, body: {\"request_id\":\"a4a43be6-69ed-4203-a6d9-84e8aee33cbf\",\"cdn_url\":\"https://cdn.eternalai.org/agents/temp_image_a4a43be6-69ed-4203-a6d9-84e8aee33cbf_1766486414.jpg\",\"status\":{\"status\":\"completed\",\"progress\":0,\"queue_position\":null,\"total_queue_size\":null,\"started_at\":null,\"estimated_wait_time\":null,\"error\":\"\"}}",
"effect_type": "image",
"rsa_pub": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlYTMxFeSVW+N/48ayqVJ\nz0wxjGy/5dZwLp9ReaeCw2LzAgNLrF3MxVtnRMS1plncR+VQYi7AkqXO144rzIw0\nDpyiOWmkXmijalY/Q/rL6kItE52nbJy7GZEWCEjQHNkiehInE0NBBOuu9BkNURB/\nmXQW/5oPlUBuTOHizmILt/su0/Sf4i8E1S0aZEZQP9jwRtkZZkPZUS4PoEOUKDXK\nTbw04LB3rWoZ/PkWonZSX0KXrboSKjUhx5NRagYtkbFw5Ru5wxLgLDdU+cS43827\n+sE91o4j5W/LJWUrRbdxiWvvz7+I//MAaAvLadqOgW+QWZx7GSIccC/1wsOJrETi\nQQIDAQAB\n-----END PUBLIC KEY-----",
"encrypted_aes_key": "I64x1QPf8VrHqxJRCFnkJ3pM+k1muu3f8t7QjlLSZ+1j2KRpOaKRDIzEfywixT+8gexroixcY/1VT0V93Pj1LnryKwViVboR6RBLSjDWIHQEswIpIIpC+cgCk1S55UyTBpIn2m7aeehFGGbFlMhBwwspCi1IU0SDbhu/zu00cqW4RFNl6QFpfm38E1uhvN/m0S64nc+yLu9LYjK4u2UU4aXfgc7FZMjyXgNDTEtYED+nfTugC3c15OXWZyzts19YqkSFqgyI7jQb9dn4Vdr3dyG5MDKUF1gKd5MuO5eJFFkCqkQODKLgoDhx4OHN+c2V0aNBfsk3hbvjJR4gIDPMSw=="
}
| Field Name | Type | Description |
|---|---|---|
request_id | string | Unique identifier for the generation request. |
status | string | Final status of the request (e.g. success, pending, failed). |
progress | integer | Progress indicator of the generation process. |
created_at | string (ISO 8601) | Timestamp when the request was created. |
updated_at | string (ISO 8601) | Timestamp of the latest status update. |
effect_type | string | Type of generation performed (e.g. image, video). |
result_url | string (URL) | Signed URL pointing to the encrypted result file (image or video). The file must be decrypted locally to be usable. |
rsa_pub | string (PEM) | User’s RSA public key in PKIX / SubjectPublicKeyInfo format (BEGIN PUBLIC KEY) used to encrypt the symmetric key. |
encrypted_aes_key | string (Base64) | AES symmetric key encrypted with the user’s RSA public key. Required to decrypt the result file. |
log | string | Internal processing log and webhook information. |
Decrypt to Get the Result
- Download the Encrypted Result
Use the result_url to download the encrypted image or video file.
- Decrypt the AES Key
Decrypt encrypted_aes_key using your RSA private key to obtain the AES symmetric key.
- Decrypt the Result File
Use the decrypted AES key to decrypt the downloaded file and retrieve the final result.
- Python
# pip install cryptography
import base64
import re
from urllib.request import urlopen
from urllib.parse import unquote
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
GCM_NONCE_SIZE = 12 # bytes
def _is_valid_base64(data: bytes) -> bool:
"""Check if data looks like base64 encoded text."""
try:
# Check if data contains only valid base64 characters
if not data:
return False
# Base64 text should be ASCII printable
decoded_str = data.decode("ascii")
# Check for base64 pattern (alphanumeric, +, /, =)
import re
if re.match(r"^[A-Za-z0-9+/=\s]+$", decoded_str):
# Try to decode and verify
base64.b64decode(decoded_str)
return True
except (UnicodeDecodeError, ValueError):
pass
return False
def _download_encrypted_data(url: str) -> bytes:
"""
Download encrypted data from URL.
Auto-detects if content is base64 encoded or raw binary.
Args:
url: URL to download from
Returns:
bytes: Raw encrypted data (nonce + ciphertext)
"""
# URL decode the URL in case it has encoded characters
decoded_url = unquote(url)
with urlopen(decoded_url) as response:
data = response.read()
# Auto-detect: try base64 decode if it looks like base64 text
if _is_valid_base64(data):
try:
return base64.b64decode(data)
except Exception:
pass
return data
def decrypt_aes_key_with_rsa(
private_key_pem: str | bytes, encrypted_aes_key_base64: str
) -> bytes:
"""
Decrypt AES key using RSA-OAEP with SHA-256.
Args:
private_key_pem: RSA private key in PEM format (PKCS#1)
encrypted_aes_key_base64: Base64 encoded encrypted AES key
Returns:
bytes: Decrypted AES key (16 bytes)
"""
if isinstance(private_key_pem, str):
private_key_pem = private_key_pem.encode("utf-8")
# Load private key
private_key = serialization.load_pem_private_key(
private_key_pem, password=None, backend=default_backend()
)
# Decode encrypted AES key from base64
encrypted_aes_key = base64.b64decode(encrypted_aes_key_base64)
# Decrypt using RSA-OAEP with SHA-256 (matching Go implementation)
aes_key = private_key.decrypt(
encrypted_aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
return aes_key
def decrypt_data_with_aes_gcm(encrypted_data: bytes, aes_key: bytes) -> bytes:
"""
Decrypt data using AES-GCM.
Args:
encrypted_data: Encrypted data in format: nonce (12 bytes) + ciphertext
aes_key: AES key (16 bytes)
Returns:
bytes: Decrypted data
"""
if len(encrypted_data) < GCM_NONCE_SIZE:
raise ValueError(
f"Encrypted data too short, expected at least {GCM_NONCE_SIZE} bytes"
)
# Extract nonce (first 12 bytes)
nonce = encrypted_data[:GCM_NONCE_SIZE]
ciphertext = encrypted_data[GCM_NONCE_SIZE:]
# Decrypt using AES-GCM
aesgcm = AESGCM(aes_key)
decrypted_data = aesgcm.decrypt(nonce, ciphertext, None)
return decrypted_data
def decrypt_with_private_key(
private_key_pem: str | bytes, encrypted_aes_key_base64: str, encrypted_data_url: str
) -> bytes:
"""
Decrypt data from URL using RSA private key and encrypted AES key.
This function:
1. Downloads encrypted data from URL (auto-detects base64 vs binary)
2. Decrypts AES key using RSA-OAEP with SHA-256
3. Decrypts data using AES-GCM
Args:
private_key_pem: RSA private key in PEM format (PKCS#1)
encrypted_aes_key_base64: Base64 encoded encrypted AES key
encrypted_data_url: URL to download encrypted data from
Returns:
bytes: Decrypted data
"""
# Step 1: Download encrypted data
encrypted_data = _download_encrypted_data(encrypted_data_url)
# Step 2: Decrypt AES key using RSA
aes_key = decrypt_aes_key_with_rsa(private_key_pem, encrypted_aes_key_base64)
# Step 3: Decrypt data using AES-GCM
decrypted_data = decrypt_data_with_aes_gcm(encrypted_data, aes_key)
return decrypted_data
# RSA private key in PEM format (PKCS#1), used to decrypt the encrypted AES session key
private_key = "-----BEGIN RSA PRIVATE KEY-----\n<YOUR_RSA_PRIVATE_KEY>\n-----END RSA PRIVATE KEY-----"
# private_key = private_key_pem
# Base64-encoded AES key encrypted using the corresponding RSA public key (RSA-OAEP + SHA-256)
encrypted_aes_key = "<ENCRYPTED_AES_KEY>"
# encrypted_aes_key = encrypted_result["encrypted_aes_key"]
# URL pointing to the encrypted output file (AES-GCM encrypted binary data: nonce + ciphertext)
result_url = "<ENCRYPTED_RESULT_URL>"
# result_url = encrypted_result["result_url"]
decrypted_data = decrypt_with_private_key(
private_key_pem = private_key,
encrypted_aes_key_base64= encrypted_aes_key,
encrypted_data_url = result_url)
with open("output.jpg", "wb") as f:
f.write(decrypted_data)