Skip to main content

gRPC API

SASHA offers a gRPC-based API for signature operations that provides an alternative to our 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 our protocol buffer specifications. You can find the complete service definition and message types in our GitHub repository: github.com/Sasha-ApS/apis

ProtobufDescription
partner_api.protoDefines the PartnerAPI service
job.protoDefines job-related message types
signature_id.protoDefines 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.

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 our Authentication Guide and Signature Concepts.