A typed .NET client for the Assinafy electronic-signature
API. It covers the complete documented HTTP surface — documents, templates, signers, assignments,
the signer-facing signing flow, signature images, tags, fields, webhooks, accounts, and users — as
strongly-typed resources on a single AssinafyClient, with one exception hierarchy, envelope
handling, and pagination already taken care of.
Targets net8.0, net9.0, and net10.0.
- Installation
- Credentials and environments
- Creating a client
- Dependency injection
- How requests and responses work
- Error handling
- The signature lifecycle
- Documents
- Templates
- Signers
- Assignments
- The signer-facing flow
- Signature images
- Public documents
- Tags
- Fields
- Webhooks
- Accounts and users
- Reference tables
- Testing
- Support matrix and versioning
- Further reading
dotnet add package Assinafy.Sdk --version 2.0.0Applications need a runtime compatible with net8.0, net9.0, or net10.0. Contributors need
.NET SDKs 8.0.424, 9.0.317, and 10.0.400; global.json selects .NET 10 for
repository commands.
Assinafy accepts either credential:
| Credential | Sent as | Obtained from | Use for |
|---|---|---|---|
| API key | X-Api-Key header |
Authentication.CreateApiKeyAsync (or the web app) |
Server-to-server integrations |
| Access token | Authorization: Bearer … |
Authentication.LoginAsync / SocialLoginAsync |
Acting as a signed-in user |
The two are mutually exclusive; supplying both throws a ValidationException. Create a separate
user for an API-key integration so it can be granted only the access it needs, and never commit the
key.
Develop against the sandbox, then switch a single option to go live:
| Environment | API base URL | Web app |
|---|---|---|
| Sandbox | https://sandbox.assinafy.com.br/v1 |
https://app-sandbox.assinafy.com.br |
| Production (default) | https://api.assinafy.com.br/v1 |
https://app.assinafy.com.br |
Most endpoints are scoped to a workspace account. Set AccountId once on the client and every
account-scoped method uses it; each of those methods also takes an optional accountId override.
Accounts.ListAsync() discovers the IDs available to the current credential.
using Assinafy.Sdk;
using Assinafy.Sdk.Models;
using var client = new AssinafyClient(new AssinafyClientOptions
{
ApiKey = Environment.GetEnvironmentVariable("ASSINAFY_API_KEY"),
AccountId = Environment.GetEnvironmentVariable("ASSINAFY_ACCOUNT_ID"),
BaseUrl = "https://sandbox.assinafy.com.br/v1", // omit for production
Timeout = TimeSpan.FromSeconds(30), // default
});Two shorthands exist for common cases:
using var fromArgs = AssinafyClient.Create(apiKey, accountId);
using var fromSettings = AssinafyClient.FromConfig(new Dictionary<string, string?>
{
["api_key"] = configuration["Assinafy:ApiKey"],
["account_id"] = configuration["Assinafy:AccountId"],
});FromConfig accepts snake_case or camelCase keys (api_key/apiKey, account_id/accountId,
token/access_token/accessToken, base_url/baseUrl).
Lifetime. AssinafyClient is thread-safe and holds a pooled HttpClient. Create one per
application and reuse it; creating one per request exhausts sockets. Dispose it only when it owns
its transport — the constructors above do; the HttpClient overload does not, leaving that client's
lifetime to you.
Transport hardening. BaseUrl must be an absolute HTTPS URL whose path is exactly /v1; user
info, extra path segments, query strings, and fragments are rejected. An SDK-owned transport
disables automatic redirects, because .NET forwards custom headers such as X-Api-Key to a
redirect target. If you supply your own HttpClient, its BaseAddress must match BaseUrl and
you must disable redirects on its primary handler:
using var handler = new SocketsHttpHandler { AllowAutoRedirect = false };
using var http = new HttpClient(handler)
{
BaseAddress = new Uri("https://sandbox.assinafy.com.br/v1/"),
};
using var client = new AssinafyClient(
new AssinafyClientOptions
{
ApiKey = apiKey,
AccountId = accountId,
BaseUrl = "https://sandbox.assinafy.com.br/v1",
},
http);AssinafyClient.CreatePrimaryHandler() returns exactly the handler the SDK uses for its own
transport — redirects disabled, five-minute pooled connection lifetime — so
new HttpClient(AssinafyClient.CreatePrimaryHandler()) is the one-line version of the above.
Credentials are attached per request, so a supplied HttpClient's default headers are never
mutated and the instance stays safe to share. Its Timeout is left untouched — set it yourself.
The package has no NuGet dependencies and ships no container adapter, so it never drags a
Microsoft.Extensions.* version into your application. Register it with the DI stack you already
have — the HttpClient constructor is the extension point:
builder.Services
.AddHttpClient("Assinafy", http =>
{
http.BaseAddress = new Uri("https://api.assinafy.com.br/v1/"); // note the trailing slash
http.Timeout = TimeSpan.FromSeconds(30);
})
.ConfigurePrimaryHttpMessageHandler(AssinafyClient.CreatePrimaryHandler)
.SetHandlerLifetime(Timeout.InfiniteTimeSpan);
builder.Services.AddSingleton(serviceProvider => new AssinafyClient(
new AssinafyClientOptions
{
ApiKey = builder.Configuration["Assinafy:ApiKey"],
AccountId = builder.Configuration["Assinafy:AccountId"],
BaseUrl = "https://api.assinafy.com.br/v1",
},
serviceProvider.GetRequiredService<IHttpClientFactory>().CreateClient("Assinafy")));Then inject AssinafyClient anywhere. Four details matter:
ConfigurePrimaryHttpMessageHandler(AssinafyClient.CreatePrimaryHandler)disables automatic redirects, so an API key is never forwarded to a redirect target. Do not skip it.SetHandlerLifetime(Timeout.InfiniteTimeSpan)— the singleton captures oneHttpClient, so it cannot observe factory handler rotation.CreatePrimaryHandlerrecycles connections throughPooledConnectionLifetimeinstead, which is what keeps DNS changes visible.BaseAddressmust matchBaseUrl, with a trailing slash on theUri.- Set
Timeouton theHttpClient. TheAssinafyClientOptions.Timeoutvalue is ignored for a supplied client, because the SDK does not mutate a transport it does not own.
Resilience policies and extra handlers chain onto the IHttpClientBuilder as usual:
builder.Services
.AddHttpClient("Assinafy", /* … */)
.ConfigurePrimaryHttpMessageHandler(AssinafyClient.CreatePrimaryHandler)
.AddStandardResilienceHandler();Do not dispose the resolved client yourself; IHttpClientFactory owns the transport.
The envelope. Every Assinafy response is { status, message, data }. The SDK unwraps it: your
method returns the data payload, already typed. A status of 400 or above becomes an
ApiException regardless of the HTTP status line, so a "200 OK" carrying an error envelope still
throws.
Naming. Request and response bodies use snake_case, handled for you. The single exception is
the signer sign body, which the API defines in camelCase; SignAssignmentValue applies that
automatically.
Lists. Two shapes exist, and the return type tells you which:
IReadOnlyList<T>— the endpoint returns a complete array (tags, activities, statistics, event types, field types, statuses).PaginatedResult<T>— the endpoint pages.Dataholds the page;MetacarriesCurrentPage,PerPage,Total, andLastPage, parsed from theX-Pagination-*response headers.Metaisnullwhen the response carried no such headers.
Paging parameters are page (1-based) and per-page (max 100), alongside search and sort
where the endpoint supports them:
var page = await client.Documents.ListAsync(new Dictionary<string, string?>
{
["status"] = "pending_signature",
["sort"] = "-created_at",
["page"] = "1",
["per-page"] = "50",
});
Console.WriteLine($"{page.Data.Count} of {page.Meta?.Total} documents");
while (page.Meta is { CurrentPage: int current, LastPage: int last } && current < last)
{
page = await client.Documents.ListAsync(new Dictionary<string, string?>
{
["page"] = (current + 1).ToString(),
["per-page"] = "50",
});
// …process page.Data
}Cancellation and timeouts. Every method takes a trailing CancellationToken. A client-side
timeout surfaces as NetworkException; a token you cancelled yourself propagates as
OperationCanceledException, unchanged.
Rate limiting. The API returns 429 when a caller exceeds its quota. The SDK does not retry
automatically — surface it, back off, and retry, or chain a resilience handler onto the
IHttpClientBuilder shown in Dependency injection.
Documents.WaitUntilReadyAsync is the one exception: it treats 404, 429, and 5xx as transient
while polling.
Every SDK-specific exception derives from AssinafyException:
| Exception | Raised when | Key members |
|---|---|---|
ValidationException |
The SDK rejects input before any HTTP call | Details (field-level) |
ApiException |
The API returned an error status or envelope | StatusCode, ApiMessage, Details |
NetworkException |
Connection, DNS, or TLS failure, or a client-side timeout | InnerException |
SerializationException |
A body could not be serialized, or a success response did not match the expected envelope or payload | InnerException |
Standard .NET argument, cancellation, disposal, and stream exceptions keep their platform types.
try
{
await client.Documents.GetAsync(documentId);
}
catch (ApiException ex) when (ex.StatusCode == 404)
{
Console.WriteLine($"Not found: {ex.ApiMessage}");
}
catch (ApiException ex) when (ex.StatusCode == 429)
{
// Back off and retry.
}
catch (ApiException ex)
{
Console.WriteLine($"{ex.StatusCode}: {ex.ApiMessage}");
Console.WriteLine(ex.Details?.GetRawText()); // structured field errors, when supplied
}
catch (NetworkException ex)
{
Console.WriteLine($"Transport failure: {ex.Message}");
}A signature request moves through five stages. Everything else in this SDK supports one of them.
- Upload a PDF into a workspace, producing a document in
uploadedstatus. - Wait for the platform to normalize it and extract pages (
metadata_ready). - Create signers — reusable people records belonging to the workspace.
- Create an assignment, binding signers to the document. This is what sends the invitations.
- Signers sign, and once the last one finishes the document becomes
certificatedand its signed artifacts become downloadable.
End to end:
using Assinafy.Sdk;
using Assinafy.Sdk.Models;
using var client = new AssinafyClient(new AssinafyClientOptions
{
ApiKey = Environment.GetEnvironmentVariable("ASSINAFY_API_KEY"),
AccountId = Environment.GetEnvironmentVariable("ASSINAFY_ACCOUNT_ID"),
});
// 1–2. Upload and wait for the document to be ready.
await using var pdf = File.OpenRead("contract.pdf");
var document = await client.Documents.UploadAsync(pdf, "contract.pdf");
await client.Documents.WaitUntilReadyAsync(document.Id);
// 3. Create the signer.
var signer = await client.Signers.CreateAsync(new CreateSignerRequest
{
FullName = "John Doe",
Email = "john@example.com",
});
// 4. Request the signature. This sends the invitation.
var assignment = await client.Assignments.CreateAsync(document.Id, new CreateAssignmentRequest
{
Method = AssignmentMethods.Virtual,
Message = "Please review and sign.",
Signers =
[
new SignerRef
{
Id = signer.Id,
VerificationMethod = SignerChannels.Email,
NotificationMethods = [SignerChannels.Email],
},
],
});
// assignment.SigningUrls carries a per-signer link if you would rather deliver it yourself.
// 5. Wait for signing to finish, then download the certified PDF.
using var deadline = new CancellationTokenSource(TimeSpan.FromHours(1));
DocumentDetails completed;
do
{
await Task.Delay(TimeSpan.FromSeconds(10), deadline.Token);
completed = await client.Documents.GetAsync(document.Id, deadline.Token);
}
while (!string.Equals(completed.Status, "certificated", StringComparison.OrdinalIgnoreCase));
var certified = await client.Documents.DownloadAsync(document.Id);
await File.WriteAllBytesAsync("contract-signed.pdf", certified);Polling is shown for clarity. In production, subscribe to the document_ready webhook
instead of polling.
Steps 1 through 4 collapse into a single call when you do not need to inspect the intermediate results:
await using var pdf = File.OpenRead("contract.pdf");
var result = await client.UploadAndRequestSignaturesAsync(new UploadAndRequestSignaturesOptions
{
FileStream = pdf,
FileName = "contract.pdf",
Message = "Please review and sign.",
Signers =
[
new UploadAndRequestSignaturesSigner
{
FullName = "John Doe",
Email = "john@example.com",
VerificationMethod = SignerChannels.Email,
NotificationMethods = [SignerChannels.Email],
},
],
});
// result.Document, result.Assignment, result.SignerIdsThe helper is deliberately not transactional, because the API has no transaction spanning upload, signer creation, and assignment creation. If a later request fails, the resources already created remain for you to inspect or clean up.
| Method | What it does | Requires |
|---|---|---|
AssignmentMethods.Virtual |
Signers are notified and sign remotely, at their convenience | Document in uploaded, metadata_processing, or metadata_ready |
AssignmentMethods.Collect |
Field values are collected in-session against explicit page and field placements | Document in metadata_ready, plus Entries |
A collect assignment needs an entry per page describing which signer fills which field:
var collect = await client.Assignments.CreateAsync(document.Id, new CreateAssignmentRequest
{
Method = AssignmentMethods.Collect,
Signers = [new SignerRef { Id = signer.Id }],
Entries =
[
new AssignmentEntry
{
PageId = document.Pages[0].Id,
Fields =
[
new AssignmentEntryField
{
SignerId = signer.Id,
FieldId = fieldId,
DisplaySettings = new DisplaySettings
{
Left = 100, Top = 640, Width = 220, Height = 40, FontSize = 12,
},
},
],
},
],
});When the signers do not exist yet, UploadAndRequestSignaturesAsync exposes EntriesFactory,
which runs after signer creation and receives the new IDs:
EntriesFactory = signerIds =>
[
new AssignmentEntry
{
PageId = pageId,
Fields = [new AssignmentEntryField { SignerId = signerIds[0], FieldId = fieldId }],
},
],Assignments consume plan documents and notification credits. Every committing call has a matching estimate that charges nothing:
var estimate = await client.Assignments.EstimateCostAsync(document.Id, request);
if (!estimate.HasSufficientResources)
throw new InvalidOperationException(estimate.BlockingReason);
Console.WriteLine($"{estimate.TotalCredits} credits, {estimate.CreditBalance} available");Documents.EstimateCostFromTemplateAsync and Assignments.EstimateResendCostAsync do the same for
the template and resend flows.
// Upload — PDF only, 25MB maximum.
await using var pdf = File.OpenRead("contract.pdf");
var document = await client.Documents.UploadAsync(pdf, "contract.pdf");
// Read
var details = await client.Documents.GetAsync(document.Id);
var listing = await client.Documents.ListAsync();
var matches = await client.Documents.SearchAsync("contract", perPage: 20);
var statuses = await client.Documents.ListStatusesAsync();
var timeline = await client.Documents.ActivitiesAsync(document.Id);
// Rename — only before an assignment exists; the server normalizes the result.
var renamed = await client.Documents.RenameAsync(document.Id, "Contract 2026");
// Binary artifacts
var original = await client.Documents.DownloadAsync(document.Id, DocumentArtifactNames.Original);
var pades = await client.Documents.DownloadAsync(document.Id, DocumentArtifactNames.Pades);
var thumbnail = await client.Documents.ThumbnailAsync(document.Id);
var pageImage = await client.Documents.DownloadPageAsync(document.Id, details.Pages[0].Id);
// Public verification of a signed document, by its signature hash — no credentials needed.
var verification = await client.Documents.VerifyAsync(signatureHash);
await client.Documents.DeleteAsync(document.Id);ListAsync accepts status, method, search, tags (comma-separated tag IDs, matching
documents that carry all of them), sort, page, and per-page. SearchAsync hits the compact
search route, which omits the expanded assignment and pages that ListAsync returns.
Three local helpers save round trips of your own:
var ready = await client.Documents.WaitUntilReadyAsync(document.Id, maxWait: TimeSpan.FromMinutes(2));
var signed = await client.Documents.IsFullySignedAsync(document.Id);
var progress = await client.Documents.GetSigningProgressAsync(document.Id);
Console.WriteLine($"{progress.Signed}/{progress.Total} ({progress.Percentage}%)");A template is a reusable PDF with named roles and field placements. Instantiating one produces an ordinary document.
await using var file = File.OpenRead("nda.pdf");
var template = await client.Templates.CreateAsync(file, "nda.pdf", name: "Mutual NDA");
// Pages and roles appear once processing finishes.
var ready = await client.Templates.GetAsync(template.Id);
var pageImage = await client.Templates.DownloadPageAsync(ready.Id, ready.Pages[0].Id);
var templates = await client.Templates.ListAsync(new Dictionary<string, string?>
{
["search"] = "nda",
});
await client.Templates.UpdateAsync(template.Id, new UpdateTemplateRequest
{
Name = "Mutual NDA v2",
Message = "Please review and sign.",
});
// Instantiate: bind existing signers to the template's roles.
var fromTemplate = await client.Documents.CreateFromTemplateAsync(
template.Id,
[new TemplateSigner { RoleId = ready.Roles[0].Id, Id = signer.Id }],
new CreateDocumentFromTemplateOptions
{
Name = "NDA — Acme",
ExpiresAt = "2026-12-31T23:59:59Z",
EditorFields = [new TemplateEditorField { FieldId = fieldId, Value = "Acme Inc." }],
});
await client.Templates.DeleteAsync(template.Id);The template display name is carried as the multipart file name, so CreateAsync appends .pdf to
name when needed rather than sending a second form field.
A signer is a workspace-level person record, reused across documents.
var signer = await client.Signers.CreateAsync(new CreateSignerRequest
{
FullName = "John Doe",
Email = "john@example.com",
WhatsAppPhoneNumber = "+5511999999999", // E.164; normalized on save
});
var page = await client.Signers.ListAsync(new Dictionary<string, string?> { ["search"] = "john" });
var one = await client.Signers.GetAsync(signer.Id);
var byMail = await client.Signers.FindByEmailAsync("john@example.com"); // exact match, or null
await client.Signers.UpdateAsync(signer.Id, new UpdateSignerRequest { GovernmentId = "00000000000" });
await client.Signers.DeleteAsync(signer.Id);FindByEmailAsync follows every result page and returns the first exact, case-insensitive match —
the server-side search is a fuzzy filter, so it alone is not enough. Email and WhatsApp number
cannot be changed while the signer has a verified channel on a document that is still in flight.
var assignments = await client.Assignments.ListAsync(new AssignmentListParams { PerPage = 50 });
// Set or clear the expiration.
await client.Assignments.ResetExpirationAsync(documentId, assignmentId, "2026-12-31T23:59:59Z");
await client.Assignments.ResetExpirationAsync(documentId, assignmentId, null);
// Re-notify a single signer (estimate first — this can cost credits).
var resendCost = await client.Assignments.EstimateResendCostAsync(documentId, assignmentId, signerId);
var resend = await client.Assignments.ResendNotificationAsync(documentId, assignmentId, signerId);
// WhatsApp messages rendered for this assignment.
var messages = await client.Assignments.ListWhatsAppNotificationsAsync(documentId, assignmentId);Per-signer options on SignerRef control how each person is verified and notified:
new SignerRef
{
Id = signer.Id,
VerificationMethod = SignerChannels.Whatsapp, // how identity is proven
NotificationMethods = [SignerChannels.Email, SignerChannels.Whatsapp],
Step = 1, // signing order; same step signs in parallel
}Step drives sequential signing: signers sharing a step are notified together, and the next step is
notified only once the previous one completes. SignerChannels.Whatsapp is paid-only and costs
extra credits. SignerChannels.DigitalCertificate is a verification method only.
These endpoints belong to the person signing, not to your workspace. They authenticate with the per-assignment signer access code carried in the signing link, and the SDK deliberately does not attach your API key or bearer token to any of them.
Implement them when you host the signing experience yourself; skip the whole section if you let Assinafy notify signers and host the signing page.
// Load everything the signer needs.
var toSign = await client.Signing.GetAsync(signerAccessCode);
var profile = await client.Signers.GetSelfAsync(signerAccessCode);
// Record acceptance of the terms.
await client.Signers.AcceptTermsAsync(signerAccessCode);
// Verify a one-time code delivered by email or WhatsApp.
await client.Signers.VerifyAsync(signerAccessCode, verificationCode);
// Virtual assignments require confirmed signer data before signing; otherwise the API returns 400.
await client.Signers.ConfirmDataAsync(documentId, signerAccessCode, new ConfirmSignerDataRequest
{
FullName = "John Doe",
Email = "john@example.com",
GovernmentId = "00000000000",
});
// Submit the field values.
await client.Signing.SignAsync(documentId, assignmentId, signerAccessCode,
[
new SignAssignmentValue
{
ItemId = item.Id,
FieldId = item.FieldId,
PageId = item.PageId,
Value = "John Doe",
},
]);
// Or decline, with a reason.
await client.Signing.DeclineAsync(documentId, assignmentId, signerAccessCode, "Wrong counterparty");A signer with several pending documents can act on them in bulk, browse their own documents, and download finished artifacts:
await client.Signing.SignMultipleAsync(signerAccessCode, [documentId1, documentId2]);
await client.Signing.DeclineMultipleAsync(signerAccessCode, [documentId3], "Not applicable");
var current = await client.Signing.GetCurrentDocumentAsync(signerId, signerAccessCode);
var mine = await client.Signing.ListDocumentsAsync(signerId, signerAccessCode,
new SignerDocumentListParams { PerPage = 25 });
var found = await client.Signing.SearchDocumentsAsync(signerId, signerAccessCode,
new SignerDocumentListParams { Search = "nda" });
var artifact = await client.Signing.DownloadPublicAsync(
signerId, documentId, DocumentArtifactNames.Certificated);When an assignment's verification method is DigitalCertificate, signing is a two-step Web PKI
exchange instead of a field submission:
var operation = await client.Signing.StartCertificateAsync(signerAccessCode);
var signedToken = await SignWithWebPkiAsync(operation.Token); // your browser/Web PKI bridge
var result = await client.Signing.CompleteCertificateAsync(signerAccessCode, signedToken);
Console.WriteLine(result.SignerName); // read from the certificateBoth routes are production-only deployed extensions: the sandbox does not expose them and they are absent from the published OpenAPI document. They require a real production certificate assignment and a browser-signed Web PKI token.
A signer's drawn signature and initials, stored for reuse across documents.
await using var png = File.OpenRead("signature.png");
await client.Signatures.UploadAsync(
png,
signerAccessCode,
reuse: true, // allow reuse in future signing processes
type: SignatureImageTypes.Signature); // or SignatureImageTypes.Initial
var image = await client.Signatures.DownloadAsync(signerAccessCode, SignatureImageTypes.Signature);Signer.HasSignature, HasInitial, and IsSignatureReusable (returned by GetSelfAsync) tell you
whether a stored image exists and whether the signer agreed to reuse it. When
IsSignatureReusable is false, do not pre-render the stored image even if one exists.
Unauthenticated lookups for a recipient who has a document link.
var info = await client.PublicDocuments.GetDetailsAsync(documentId);
await client.PublicDocuments.SendTokenAsync(documentId, "john@example.com");SendTokenAsync asks the API to email a signing access token; omit the address to use the
document's configured recipient.
Tags are unique per workspace, case-insensitively, and attach to documents.
var tag = await client.Tags.CreateAsync(new CreateTagRequest { Name = "Contracts", Color = "3366FF" });
var tags = await client.Tags.ListAsync(search: "con");
await client.Tags.UpdateAsync(tag.Id, new UpdateTagRequest { Color = "FF6600" });
await client.Tags.UpdateAsync(tag.Id, new UpdateTagRequest { ClearColor = true });
// Attach keeps existing tags; set replaces the whole set (pass [] to clear).
var attached = await client.Tags.AddToDocumentAsync(documentId, [tag.Id]);
var replaced = await client.Tags.SetForDocumentAsync(documentId, [tag.Id]);
var onDoc = await client.Tags.ListForDocumentAsync(documentId);
await client.Tags.RemoveFromDocumentAsync(documentId, tag.Id);
await client.Tags.DeleteAsync(tag.Id, force: true); // force detaches everywhere firstCreating a duplicate name returns 409. Deleting a tag that is still attached returns 409 unless
force is true. DeleteWithResultAsync and RemoveFromDocumentWithResultAsync are variants that
return the API's { deleted } / { detached } payload instead of nothing.
Field definitions describe the typed inputs a signer fills in — with an optional regular expression the platform validates against.
var types = await client.Fields.ListTypesAsync();
var field = await client.Fields.CreateAsync(new CreateFieldDefinitionRequest
{
Name = "Customer reference",
Type = "text",
Regex = "^[A-Z]{3}-[0-9]{4}$",
IsRequired = true,
});
var fields = await client.Fields.ListAsync(new FieldListParams { IncludeStandard = true });
await client.Fields.UpdateAsync(field.Id, new UpdateFieldDefinitionRequest { IsActive = false });
await client.Fields.UpdateAsync(field.Id, new UpdateFieldDefinitionRequest { ClearRegex = true });
// Validate before submitting, either as the account or on a signer's behalf.
var check = await client.Fields.ValidateAsync(
field.Id,
new ValidateFieldValueRequest { Value = "ACM-1234" },
signerAccessCode);
var checks = await client.Fields.ValidateMultipleAsync(
[
new ValidateFieldValueItem { FieldId = field.Id, Value = "ACM-1234" },
]);
await client.Fields.DeleteAsync(field.Id);Deleting a field already used on a document fails.
A workspace has one subscription. Prefer it over polling for document state.
var events = await client.Webhooks.ListEventTypesAsync();
await client.Webhooks.UpdateSubscriptionAsync(new UpdateWebhookSubscriptionRequest
{
Url = "https://example.com/webhooks/assinafy",
Email = "ops@example.com",
IsActive = true,
Events = ["document_ready", "signer_signed_document", "signer_rejected_document"],
});
var subscription = await client.Webhooks.GetAsync();
// Delivery history and replay.
var history = await client.Webhooks.ListDispatchesAsync(new ListDispatchesParams
{
Delivered = false,
From = DateTimeOffset.UtcNow.AddDays(-7).ToUnixTimeSeconds(),
PerPage = 50,
});
foreach (var failed in history.Data)
await client.Webhooks.RetryDispatchAsync(failed.Id);
// Pause delivery without losing the configuration.
await client.Webhooks.InactivateAsync();There is no delete endpoint — InactivateAsync, or an update with IsActive = false, is how you
stop deliveries.
Deliveries arrive as the same { status, message, data } envelope your endpoint should acknowledge
with a 2xx. assignment_created and document_metadata_ready have no guaranteed ordering, and
unknown fields are forward-compatible additions — ignore rather than reject them. The full event
catalog, payload keys, and delivery contract are in
docs/API.md.
var accounts = await client.Accounts.ListAsync(); // discover account IDs
var account = await client.Accounts.GetAsync();
var theme = await client.Accounts.GetThemeAsync(); // branding colors and logo URL
await client.Accounts.UpdateAsync(new UpdateAccountRequest
{
Name = "Acme Legal",
NotificationSenderType = AccountNotificationSenderTypes.Account,
});
await using var logo = File.OpenRead("logo.png");
await client.Accounts.UploadLogoAsync(logo, "logo.png");
var logoBytes = await client.Accounts.DownloadLogoAsync();
await client.Accounts.DeleteLogoAsync();
var user = await client.Users.GetSelfAsync();
var prefs = await client.Users.GetNotificationPreferencesAsync();
await client.Users.UpdateNotificationPreferencesAsync(new UpdateNotificationPreferencesRequest
{
DocumentCompleted = true,
DocumentExpired = false,
});NotificationSenderType decides whether signers see the individual user or the workspace as the
sender. Statistics are available per account and summed across every account the user belongs to:
var monthly = await client.Accounts.GetStatsAsync(new DocumentStatsParams
{
Granularity = DocumentStatsGranularities.Monthly,
});
var daily = await client.Users.GetStatsAsync(new DocumentStatsParams
{
Granularity = DocumentStatsGranularities.Daily,
Month = "2026-08", // required for daily
});
foreach (var row in monthly)
Console.WriteLine($"{row.Period}: {row.DocumentsSent} sent, {row.DocumentsCertified} certified");API keys are managed through the Authentication resource. Generating a new key replaces the
previous one, and the full value is shown only once:
var created = await client.Authentication.CreateApiKeyAsync(new CreateApiKeyRequest { Password = password });
var masked = await client.Authentication.GetApiKeyAsync();
await client.Authentication.DeleteApiKeyAsync();Document artifacts (DocumentArtifactNames)
| Value | Contents |
|---|---|
original |
The uploaded PDF, unchanged |
certificated |
The signed and certificated PDF (default) |
certificate-page |
The standalone certificate page |
pades |
The signed PDF in PAdES format |
bundle |
The signed PDF bundled with the certificate page |
Channels (SignerChannels) — Email, Whatsapp (paid, extra cost), DigitalCertificate
(verification only). Values are capitalized exactly as shown.
Assignment methods (AssignmentMethods) — virtual, collect.
Signature image types (SignatureImageTypes) — signature, initial.
Statistics granularity (DocumentStatsGranularities) — monthly, daily (requires Month).
Notification sender (AccountNotificationSenderTypes) — User, Account.
Document status codes are not fixed constants; retrieve the live list, with each status's deletion
rule, from Documents.ListStatusesAsync(). The statuses WaitUntilReadyAsync treats as ready are
metadata_ready, pending_signature, and certificated.
The regular suite runs on xUnit v3 over Microsoft.Testing.Platform against a stubbed HTTP transport,
on every supported target framework. Arguments after -- are runner options:
dotnet test --solution Assinafy.Sdk.sln -- --filter-not-trait "Category=Live"Live tests run against the sandbox only, and fail fast when a credential is missing or the base URL is not exactly the sandbox:
ASSINAFY_API_KEY=... \
ASSINAFY_ACCOUNT_ID=... \
ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1 \
dotnet test --project tests/Assinafy.Sdk.Tests/Assinafy.Sdk.Tests.csproj \
--framework net10.0 -- --filter-trait "Category=Live"ASSINAFY_TEST_EMAIL_PRIMARY and ASSINAFY_TEST_EMAIL_SECONDARY are optional overrides; without
them the suite uses reserved example.com addresses, so the GitHub sandbox environment needs only
the two secrets above.
The sandbox suite does not exercise the production-only certificate routes. Local transport tests cover their request construction, credential isolation, and response deserialization; the complete flow needs a production certificate assignment and a browser-signed Web PKI token.
| Target | .NET support | Ends |
|---|---|---|
net8.0 |
LTS, maintenance | 2026-11-10 |
net9.0 |
STS, maintenance | 2026-11-10 |
net10.0 |
LTS, active | 2028-11-14 |
The package has no NuGet dependencies, so it never constrains which Microsoft.Extensions.*
version your application resolves.
Versioning is semantic. .NET package validation runs at pack time, so a binary-breaking change
cannot ship in a patch. Methods kept only for older call sites are marked [Obsolete] with the
replacement named in the message.
Upgrading from 1.x: services.AddAssinafy(...) was removed along with the SDK's
Microsoft.Extensions.* dependencies. Replace it with the registration in
Dependency injection — roughly ten lines in your composition root, using
packages your ASP.NET Core app already references. No other API changed.
- docs/API.md — the complete reference: every public SDK method with its full signature, and all 89 production operations with request and response payloads, error bodies, authentication, and the webhook contract.
- docs/openapi.json — the checked-in production OpenAPI snapshot. CI verifies it still matches the live document on every run.
- CHANGELOG.md — release history.
- SECURITY.md — vulnerability reporting.
- LICENSE — MIT.