Skip to main content

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

Output Video with "Zoom out" visual effect

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"
}'
ParameterTypeRequiredDescription
imagesarrayYesA image URL or Base64-encoded image (≤ 5 MB) used as input for the Transform Visual Effect process.
effect_idstringYes

The unique identifier of the Visual Effect to apply. The effect_id determines the transformation behavior and the output type (image or video).
Get list of effect_id

durationintNo

Duration of the video effect in seconds. Only used for video effects.
Range: 1 - 5

audiobooleanNo

Only used for video effects.
Default: true

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
}
ParameterTypeDescription
request_idstringUnique identifier of the request to get result
statusstringCurrent status of the request (e.g. pending, success).
resultstringResult data returned after processing.
progressintegerProcessing 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"
}
ParameterTypeDescription
request_idstringUnique identifier for the generation request.
statusstringFinal status of the request (e.g. success, pending, failed).
progressintegerProgress indicator of the generation process.
created_atstring (ISO 8601)Timestamp when the request was created.
updated_atstring (ISO 8601)Timestamp of the latest status update.
result_urlstringURL of the generated output image or video.
logstringInternal processing log and webhook information.
effect_typestringType 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

  1. User generates an RSA key pair: Public Key / Private Key
  2. User sends the public key with the API request.
  3. 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
  4. Server returns only encrypted output + encrypted symmetric key.
  5. 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


openssl genrsa 2048 | tee private_key.pem | openssl rsa -pubout > public_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


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-----"
}'
ParameterTypeRequiredDescription
rsa_pubstringOptional (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.


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)

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 NameTypeDescription
request_idstringUnique identifier for the generation request.
statusstringFinal status of the request (e.g. success, pending, failed).
progressintegerProgress indicator of the generation process.
created_atstring (ISO 8601)Timestamp when the request was created.
updated_atstring (ISO 8601)Timestamp of the latest status update.
effect_typestringType of generation performed (e.g. image, video).
result_urlstring (URL)Signed URL pointing to the encrypted result file (image or video). The file must be decrypted locally to be usable.
rsa_pubstring (PEM)User’s RSA public key in PKIX / SubjectPublicKeyInfo format (BEGIN PUBLIC KEY) used to encrypt the symmetric key.
encrypted_aes_keystring (Base64)AES symmetric key encrypted with the user’s RSA public key. Required to decrypt the result file.
logstringInternal processing log and webhook information.

Decrypt to Get the Result

  1. Download the Encrypted Result

Use the result_url to download the encrypted image or video file.

  1. Decrypt the AES Key

Decrypt encrypted_aes_key using your RSA private key to obtain the AES symmetric key.

  1. Decrypt the Result File

Use the decrypted AES key to decrypt the downloaded file and retrieve the final result.


# 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)