Quickstart Tutorial
Protect your first image and get back its SignatureID. The tutorial takes about 10 minutes.
Steps
- Get an access token.
- Submit an image.
- Poll the job until it completes.
- Download the protected image.
Before you start
- A Client ID and Client Secret from SASHA (Prerequisites)
- A JPEG or PNG image, as a public URL or a local file
- A terminal
This tutorial polls for job status. In production, set up callbacks instead: SASHA posts the finished job to your endpoint, so your service makes no status requests.
For gRPC you also need a gRPC client such as nice-grpc, and client code generated from the SASHA .proto files.
Step 1: Get an access token
Exchange your client credentials for an access token.
- REST
- gRPC
Replace YOUR_CLIENT_ID and YOUR_CLIENT_SECRET with your own credentials:
curl https://partner.api.sasha.eu/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-d "grant_type=client_credentials"
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}
Keep the access_token value for the next steps. The token expires after expires_in seconds.
import { createChannel, createClient } from "nice-grpc";
import { PartnerAPIClient } from "./generated/partner_api_grpc_pb.js";
const channel = createChannel("partner.api.sasha.eu:443");
const client = createClient(PartnerAPIClient, channel);
const authResponse = await client.authenticateWithClientCredential({
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
});
const accessToken = authResponse.accessToken;
console.log("Access token:", accessToken);
Keep the accessToken value for the next steps.
Step 2: Submit the image
Submit the image URL to the Partner API. SASHA fetches the image and returns a job.
- REST
- gRPC
curl https://partner.api.sasha.eu/signature/embed \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"media_url": "https://developer.sasha.eu/img/sasha-logo-wall.jpeg",
"media_mime_type": "image/jpeg"
}'
Response:
{
"job": {
"job_id": "44cab986-0385-470a-8e5c-c657b0543d19",
"type": "embed-signature",
"status": "pending",
"created_at": "2025-10-07T12:00:00Z",
"updated_at": "2025-10-07T12:00:00Z"
}
}
Keep the job_id value. Step 3 uses it to read the job status.
To send a local file, use multipart/form-data (REST) or the EmbedSignatureFromData streaming method (gRPC). See Embed Signature.
import { createChannel, createClient, Metadata } from "nice-grpc";
import { PartnerServiceClient } from "./generated/partner_grpc_pb.js";
const partnerChannel = createChannel("partner.api.sasha.eu:443");
const partnerClient = createClient(PartnerServiceClient, partnerChannel);
// Create metadata with authorization token
const metadata = new Metadata();
metadata.set("authorization", `Bearer ${accessToken}`);
// Submit image for protection
const embedResponse = await partnerClient.embedSignatureFromURL(
{
mediaUrl: "https://developer.sasha.eu/img/sasha-logo-wall.jpeg",
mediaMimeType: "image/jpeg",
},
{ metadata }
);
const jobId = embedResponse.job.jobId;
console.log("Job ID:", jobId);
console.log("Job status:", embedResponse.job.status);
To send a local file, use the EmbedSignatureFromData streaming method. See the gRPC API Reference.
Step 3: Poll the job
Read the job every 2 seconds until status is completed. Most jobs complete within 1 second.
- REST
- gRPC
curl https://partner.api.sasha.eu/jobs/44cab986-0385-470a-8e5c-c657b0543d19 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response (while processing):
{
"job_id": "44cab986-0385-470a-8e5c-c657b0543d19",
"type": "embed-signature",
"status": "in_progress",
"created_at": "2025-10-07T12:00:00Z",
"updated_at": "2025-10-07T12:00:01Z"
}
Response (when complete):
{
"job_id": "44cab986-0385-470a-8e5c-c657b0543d19",
"type": "embed-signature",
"status": "completed",
"output_url": "https://storage.sasha.eu/protected/abc123.jpg",
"output_url_expires_at": "2025-10-07T13:00:00Z",
"signature_id": "12345678901234567890",
"creator_id": 123,
"created_at": "2025-10-07T12:00:00Z",
"updated_at": "2025-10-07T12:00:05Z"
}
const waitForCompletion = async (jobId) => {
while (true) {
const jobResponse = await partnerClient.getJob(
{ jobId },
{ metadata }
);
console.log("Job status:", jobResponse.status);
if (jobResponse.status === "completed") {
return jobResponse;
}
if (jobResponse.status === "failed") {
throw new Error(`Job failed: ${jobResponse.error.message}`);
}
// Wait 2 seconds before reading the job again
await new Promise((resolve) => setTimeout(resolve, 2000));
}
};
const completedJob = await waitForCompletion(jobId);
console.log("Protected image URL:", completedJob.outputUrl);
console.log("Signature ID:", completedJob.signatureId);
Step 4: Download the protected image
Download the image from output_url.
- REST
- gRPC
curl -o protected-image.jpg "https://storage.sasha.eu/protected/abc123.jpg"
output_url stops working at the time in output_url_expires_at. Download the image before then.
import { writeFileSync } from "fs";
const imageResponse = await fetch(completedJob.outputUrl);
const imageBuffer = await imageResponse.arrayBuffer();
writeFileSync("protected-image.jpg", Buffer.from(imageBuffer));
console.log("Protected image saved");
The output URL stops working at the time in outputUrlExpiresAt. Download the image before then.
Result
You now have:
job_id— the identifier of the embed jobsignature_id— the public handle for this Signaturecreator_id— the CreatorID of the partner that protected the image- A protected image that looks identical to the original and carries the Signature
Look the Signature up
Confirm the Signature is in the image by looking it up.
- REST
- gRPC
curl https://partner.api.sasha.eu/signature/lookup \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"media_url": "[USE_OUTPUT_URL_FROM_STEP_3]",
"media_mime_type": "image/jpeg"
}'
Replace [USE_OUTPUT_URL_FROM_STEP_3] with the output_url from Step 3.
const lookupResponse = await partnerClient.lookupSignatureFromURL(
{
mediaUrl: completedJob.outputUrl,
mediaMimeType: "image/jpeg",
},
{ metadata }
);
// Wait for the lookup job to complete, the same way as the embed job
const lookupResult = await waitForCompletion(lookupResponse.job.jobId);
console.log("Found signature ID:", lookupResult.signatureId);
The signature_id the lookup returns matches the one from your embed job.
Common issues
| Issue | Fix |
|---|---|
401 Unauthorized | The token expired, or the credentials are wrong. Request a new token. |
400 Bad Request, media_url is required | Send media_url and media_mime_type in a JSON body. |
Job status is failed, error unsupported_format | Convert the image to JPEG or PNG. |
404 Not Found on output_url | The URL expired. Read output_url_expires_at, then run the embed job again. |
Next steps
- Authentication — token lifetime and renewal
- Signature — what a Signature is and how it survives editing
- Callbacks — receive the finished job instead of polling for it
- Rate limiting — what a
429means and how to recover - REST API Reference — every endpoint, field, and error code
- gRPC API Reference — every RPC and message
- Replace polling with callbacks.
- Cache the access token. Renew it 60 seconds before
expires_in. - Retry a failed job only on
failed_to_fetch_from_urlandinternal.