Skip to main content

gRPC API

SASHA offers a gRPC-based API for signature operations that provides an alternative to the REST API. The gRPC API is particularly useful for high-performance applications and systems that require efficient binary communication, especially when working with large image files.

Overview​

The PartnerAPI gRPC service provides functionality for:

  • Authentication using client credentials to obtain access tokens
  • Embedding signatures into images to protect them with SASHA technology
  • Looking up signatures from images to detect if they're already protected
  • Managing jobs for asynchronous processing of signature operations
  • Streaming support for efficient handling of large image files

Service Definition​

The PartnerAPI service is defined in SASHA's protocol buffer specifications. You can find the complete service definition and message types in SASHA's GitHub repository: github.com/Sasha-ApS/apis (external link)

partner_api.proto (external link)Defines the PartnerAPI service
job.proto (external link)Defines job-related message types
signature_id.proto (external link)Defines SignatureID message type

Connection Details​

Service Endpoint​

The gRPC PartnerAPI service is available at:

  • partner.api.sasha.eu:443 (TLS enabled)

Authentication​

Overview​

Partners must authenticate with the PartnerAPI.AuthenticateWithClientCredential RPC using the provided client_id and client_secret credentials, and then pass the resulting access token in the "Authorization" metadata with the format Bearer <token> for all subsequent API calls.

AuthenticateWithClientCredential​

This RPC method grants your application access to SASHA services using the OAuth 2.0 client credentials flow.

Method Signature:

rpc AuthenticateWithClientCredential(AuthenticateWithClientCredentialRequest)
returns (AuthenticateWithClientCredentialResponse) {
option idempotency_level = IDEMPOTENT;
}

Request Message:

message AuthenticateWithClientCredentialRequest {
string client_id = 1; // Required: Your application's Client ID
string client_secret = 2; // Required: Your application's Client Secret
}

Response Message:

message AuthenticateWithClientCredentialResponse {
string access_token = 1; // The access token for API calls
uint32 expires_in = 2; // Token expiration time in seconds
}

Using the Access Token​

Once you obtain an access token from AuthenticateWithClientCredential, you must include it in the metadata of all subsequent requests to PartnerAPI methods.

gRPC Metadata:

Authorization: Bearer <access-token>

Error Handling​

The PartnerAPI RPCs may return the following authentication errors:

  • Token expired If the token is expired, the service returns an "UNAUTHENTICATED" status code with the message starting with "Token expired".

  • Token missing If the token is missing, the service returns an "UNAUTHENTICATED" status code with the message starting with "Token not provided".

  • Token invalid If the token is invalid, the service returns an "UNAUTHENTICATED" status code with the message starting with "Invalid token".

  • Other authorization issues The service may return "UNAUTHENTICATED" status code for other authorization issues.

EmbedSignatureFromURL, EmbedSignatureFromData, LookupSignatureFromURL and LookupSignatureFromData also return "UNAVAILABLE" with the reason MEDIA_STAGING_FULL when SASHA is holding as much submitted media as it can until jobs already accepted finish processing. The reason is a google.rpc.ErrorInfo with domain partner-api.sasha.eu in the grpc-status-details-bin trailer, and the retry-after trailer holds the number of seconds to wait. No job was created, so the same request can be sent again after that wait without creating a second job.

Embed Signature Operations​

EmbedSignatureFromURL​

Protects an image by embedding a SASHA Signature using an image URL.

Method Signature:

rpc EmbedSignatureFromURL(EmbedSignatureFromURLRequest)
returns (EmbedSignatureFromURLResponse);

Request Message:

message EmbedSignatureFromURLRequest {
string media_url = 2; // Required: URL of the image to protect
string media_mime_type = 3; // Required: MIME type (image/jpeg, image/png, image/webp)
sasha.common.public.Record record = 4; // Optional: Record to attach to the new signature
}

Response Message:

message EmbedSignatureFromURLResponse {
Job job = 1; // The created job for async processing
}

EmbedSignatureFromData​

Protects an image by embedding a SASHA Signature using streaming image data.

Method Signature:

rpc EmbedSignatureFromData(stream EmbedSignatureFromDataRequest)
returns (EmbedSignatureFromDataResponse);

Request Message (Streaming):

message EmbedSignatureFromDataRequest {
bytes media_data_chunk = 1; // Image data chunk (can be streamed)
string media_mime_type = 2; // Required: MIME type (image/jpeg, image/png, image/webp)
}

Response Message:

message EmbedSignatureFromDataResponse {
Job job = 1; // The created job for async processing
}

Lookup Signature Operations​

LookupSignatureFromURL​

Checks if an image is already protected with SASHA Signature using an image URL.

Method Signature:

rpc LookupSignatureFromURL(LookupSignatureFromURLRequest)
returns (LookupSignatureFromURLResponse);

Request Message:

message LookupSignatureFromURLRequest {
string media_url = 2; // Required: URL of the image to check
string media_mime_type = 3; // Required: MIME type (image/jpeg, image/png, image/webp)
sasha.common.public.GuidanceContext guidance_context = 4; // Optional: where the media was found
}

LookupSignatureFromData​

Checks if an image is already protected with SASHA Signature using streaming image data.

Method Signature:

rpc LookupSignatureFromData(stream LookupSignatureFromDataRequest)
returns (LookupSignatureFromDataResponse);

Request Message (Streaming):

message LookupSignatureFromDataRequest {
bytes media_data_chunk = 1; // Image data chunk (can be streamed)
string media_mime_type = 2; // Required: MIME type (image/jpeg, image/png, image/webp)
sasha.common.public.GuidanceContext guidance_context = 3; // Optional: where the media was found
}

Record Management Operations​

Records attach distribution intent and display metadata to a signature. For what the fields mean and how they are evaluated, see Records; for worked examples of each operation, see Manage Records.

GetSignatureRecord​

Reads the Record attached to a signature the calling partner registered.

Method Signature:

rpc GetSignatureRecord(GetSignatureRecordRequest)
returns (GetSignatureRecordResponse);

Request Message:

message GetSignatureRecordRequest {
sasha.common.public.SignatureID signature_id = 1; // Required
}

Response Message:

message GetSignatureRecordResponse {
sasha.common.public.Record record = 1; // Unset when no Record is attached
uint64 revision = 2; // Current revision, for conditional writes
}

A signature with no Record attached returns OK with record unset — it is not a NOT_FOUND. The REST surface differs here and returns 404.

UpdateSignatureRecord​

Creates, replaces, patches, or removes a Record. There is one write method; the update mask decides which.

Method Signature:

rpc UpdateSignatureRecord(UpdateSignatureRecordRequest)
returns (UpdateSignatureRecordResponse);

Request Message:

message UpdateSignatureRecordRequest {
sasha.common.public.SignatureID signature_id = 1; // Required
sasha.common.public.Record record = 2; // The Record, or the fields being changed
google.protobuf.FieldMask update_mask = 3; // Required: which paths to write
bool return_record = 4; // Return the stored Record in the response
optional uint64 expected_revision = 5; // Optimistic concurrency precondition
}

Response Message:

message UpdateSignatureRecordResponse {
sasha.common.public.Record record = 1; // Present when return_record was set
uint64 revision = 2; // The signature's new revision
}

The mask selects the operation: record replaces the whole Record, record with the value left unset removes it, and record.<field> paths change only the named fields. A named path whose value is unset clears that field.

Set expected_revision to the revision last read to make the write conditional. If the signature has moved on, the call fails with FAILED_PRECONDITION and nothing changes. A write against a revoked signature also fails with FAILED_PRECONDITION, and the two are not distinguishable — retry once, and treat a second identical failure as revocation rather than contention.

Every accepted write bumps the revision, including one that changes nothing.

Job Management Operations​

All signature operations are asynchronous and return a Job object. Use these methods to manage and monitor job progress.

GetJob​

Retrieves the status and results of a specific job.

Method Signature:

rpc GetJob(GetJobRequest) returns (Job);

Request Message:

message GetJobRequest {
string job_id = 1; // Required: The job ID to retrieve
}

ListJobs​

Lists all jobs for the authenticated partner.

Method Signature:

rpc ListJobs(ListJobsRequest) returns (ListJobsResponse);

WatchJob​

Watches a job for completion using server-side streaming (alternative to polling).

Method Signature:

rpc WatchJob(WatchJobRequest) returns (stream WatchJobResponse);

Request Message:

message WatchJobRequest {
string job_id = 1; // Required: The job ID to watch
}

Response Stream:

message WatchJobResponse {
Job job = 1; // Updated job status
}

Code Examples​

Go Example - Authentication​

package main

import (
"context"
"crypto/tls"
"log"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials"

partnerapiv1 "github.com/Sasha-ApS/apis/sasha/partner-api/v1"
)

func authenticate(clientID, clientSecret string) string {
// Create TLS credentials
creds := credentials.NewTLS(&tls.Config{})

// Connect to the PartnerAPI service
conn, err := grpc.Dial("partner.api.sasha.eu:443", grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()

// Create PartnerAPI service client
client := partnerapiv1.NewPartnerAPIClient(conn)

// Authenticate with client credentials
req := &partnerapiv1.AuthenticateWithClientCredentialRequest{
ClientId: clientID,
ClientSecret: clientSecret,
}

resp, err := client.AuthenticateWithClientCredential(context.Background(), req)
if err != nil {
log.Fatalf("Authentication failed: %v", err)
}

log.Printf("Access token: %s", resp.AccessToken)
log.Printf("Expires in: %d seconds", resp.ExpiresIn)

return resp.AccessToken
}

Python Example - Authentication​

import grpc
from sasha.partner_api.v1 import partner_api_pb2, partner_api_pb2_grpc

def authenticate(client_id: str, client_secret: str) -> str:
# Create secure channel
credentials = grpc.ssl_channel_credentials()
channel = grpc.secure_channel('partner.api.sasha.eu:443', credentials)

# Create PartnerAPI service stub
stub = partner_api_pb2_grpc.PartnerAPIStub(channel)

# Create authentication request
request = partner_api_pb2.AuthenticateWithClientCredentialRequest(
client_id=client_id,
client_secret=client_secret
)

# Call the service
response = stub.AuthenticateWithClientCredential(request)

print(f"Access token: {response.access_token}")
print(f"Expires in: {response.expires_in} seconds")

return response.access_token

Go Example - Embed Signature from URL​

func embedSignatureFromURL(client partnerapiv1.PartnerAPIClient, accessToken, imageURL string) {
// Create context with authorization
ctx := metadata.AppendToOutgoingContext(context.Background(),
"authorization", "Bearer "+accessToken)

// Create embed request
req := &partnerapiv1.EmbedSignatureFromURLRequest{
MediaUrl: imageURL,
MediaMimeType: "image/jpeg",
}

// Call the service
resp, err := client.EmbedSignatureFromURL(ctx, req)
if err != nil {
log.Fatalf("Embed signature failed: %v", err)
}

log.Printf("Job created: %s", resp.Job.JobId)
log.Printf("Job status: %s", resp.Job.Status)
}

Python Example - Embed Signature from URL​

def embed_signature_from_url(stub, access_token: str, image_url: str):
# Create metadata with authorization
metadata = [('authorization', f'Bearer {access_token}')]

# Create embed request
request = signature_api_pb2.EmbedSignatureFromURLRequest(
media_url=image_url,
media_mime_type='image/jpeg'
)

# Call the service
response = stub.EmbedSignatureFromURL(request, metadata=metadata)

print(f"Job created: {response.job.job_id}")
print(f"Job status: {response.job.status}")

return response.job.job_id

Go Example - Complete Flow​

package main

import (
"context"
"crypto/tls"
"log"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"

partnerapiv1 "github.com/Sasha-ApS/apis/sasha/partner-api/v1"
)

func main() {
// Create TLS credentials
creds := credentials.NewTLS(&tls.Config{})

// Connect to the PartnerAPI service
conn, err := grpc.Dial("partner.api.sasha.eu:443", grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()

// Create PartnerAPI service client
client := partnerapiv1.NewPartnerAPIClient(conn)

// Step 1: Authenticate
authReq := &partnerapiv1.AuthenticateWithClientCredentialRequest{
ClientId: "your-client-id",
ClientSecret: "your-client-secret",
}

authResp, err := client.AuthenticateWithClientCredential(context.Background(), authReq)
if err != nil {
log.Fatalf("Authentication failed: %v", err)
}

accessToken := authResp.AccessToken
log.Printf("Authenticated successfully. Token expires in: %d seconds", authResp.ExpiresIn)

// Step 2: Embed signature
ctx := metadata.AppendToOutgoingContext(context.Background(),
"authorization", "Bearer "+accessToken)

embedReq := &partnerapiv1.EmbedSignatureFromURLRequest{
MediaUrl: "https://example.com/image.jpg",
MediaMimeType: "image/jpeg",
}

embedResp, err := client.EmbedSignatureFromURL(ctx, embedReq)
if err != nil {
log.Fatalf("Embed signature failed: %v", err)
}

jobID := embedResp.Job.JobId
log.Printf("Job created: %s", jobID)

// Step 3: Watch job for completion
watchReq := &partnerapiv1.WatchJobRequest{
JobId: jobID,
}

stream, err := client.WatchJob(ctx, watchReq)
if err != nil {
log.Fatalf("Failed to watch job: %v", err)
}

for {
resp, err := stream.Recv()
if err != nil {
log.Fatalf("Stream error: %v", err)
}

job := resp.Job
log.Printf("Job %s status: %s", job.JobId, job.Status)

if job.Status == "COMPLETED" {
log.Printf("Job completed successfully!")
if job.OutputUrl != "" {
log.Printf("Protected image URL: %s", job.OutputUrl)
}
break
} else if job.Status == "FAILED" {
log.Printf("Job failed: %s", job.ErrorMessage)
break
}
}
}

Best Practices​

Authentication​

  • Store access tokens securely and reuse them until they expire
  • Implement token refresh logic before expiration to avoid service interruptions
  • Handle authentication errors gracefully with retry logic

Streaming Large Files​

  • Use EmbedSignatureFromData and LookupSignatureFromData for large images
  • Stream data in reasonable chunks (1MB recommended)

Job Management​

  • Use WatchJob for real-time updates instead of polling GetJob
  • Implement proper error handling for stream disconnections
  • Store job IDs for later reference and status checking

Performance Optimization​

  • Reuse gRPC connections when possible
  • Use connection pooling for high-throughput applications
  • Consider parallel processing for multiple images

For more information about authentication concepts and signature operations, see the Authentication Guide and Signature Concepts.