Help us shape the future of Go! Take the 2026 Go Developer Survey

popnow

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2026 License: MIT Imports: 20 Imported by: 0

README

PopNow

PopNow Go SDK: AI Video, Image and Music Generation API for Go

popnow-go is a zero-dependency Go client for the PopNow API that turns text, images and audio into AI videos, images, music and speech, using the same models, prices and credits as popnow.ai.

Go Reference CI Docs

PopNow is a browser-based AI studio for videos, images and music: image-to-video, text-to-video, AI images, music and speech, one-tap trending video effects and image styles. This package wraps its developer API (https://api.popnow.ai/api/v1) so you can call Kling, Seedance, Wan, Vidu, MiniMax, Nano Banana, GPT Image, Flux, Suno and more from Go. Credit packs, no subscription; failed runs are refunded. Make it pop.

  • Standard library only. Go 1.23+, no third-party modules in your dependency graph.
  • Safe retries. Back-off with jitter on 429 / 500 / 503, honouring Retry-After, and only for requests that cannot charge twice: task submissions require an Idempotency-Key.
  • The whole task workflow. Quote, create (optionally waiting server-side), poll with back-off, retry, purge, task history with a cursor iterator, usage and balance.
  • Files and the asset library. Streamed three-step uploads, LoRA import from Hugging Face or Civitai, popnow://f/ and popnow://a/ URIs, saved assets and characters.
  • Webhook verification for task callbacks and for Standard Webhooks account endpoints.
  • Contract-tested against a pinned copy of the published OpenAPI spec.

Quickstart

1. Install (Go 1.23+):

go get github.com/popnow-ai/popnow-go

2. Create an API key in the API console and export it:

export POPNOW_API_KEY="sk-popnow-..."

3. Generate an image:

package main

import (
	"context"
	"log"

	popnow "github.com/popnow-ai/popnow-go"
)

func main() {
	ctx := context.Background()
	client, err := popnow.New(popnow.Options{}) // reads POPNOW_API_KEY
	if err != nil {
		log.Fatal(err)
	}

	request := popnow.CreateTaskInput{
		Model: "alibaba/z-image-turbo/text-to-image",
		Input: map[string]any{"prompt": "A small red fox curled up in a teacup, soft morning light"},
	}
	// One key per piece of work, reused on every resend: a repeat returns the
	// original task instead of generating (and charging) twice.
	task, err := client.CreateTask(ctx, request, "fox-teacup-1", popnow.CreateTaskOptions{})
	if err != nil {
		log.Fatal(err)
	}

	final, err := client.WaitForTerminal(ctx, task.TaskID, popnow.WaitOptions{})
	if err != nil {
		log.Fatal(err)
	}
	log.Println(final.State, final.Credits, "credits")
	for _, asset := range final.ReadyAssets() {
		log.Println(asset.URL) // a signed link, valid for about 20 minutes
	}
}

The import path is the module path, so go get works straight from GitHub; no registry is involved.

What it covers

Area Methods Endpoint
Models ListModels, GetModel GET /api/v1/models, GET /api/v1/models/{model}
Pricing Quote POST /api/v1/jobs/quote
Tasks CreateTask, GetTask, WaitForTerminal, RetryTask, PurgeTask POST /jobs/createTask, GET /jobs/recordInfo, POST /jobs/retry, POST /jobs/purge
History ListTasks, EachTask, Tasks (iterator) GET /api/v1/jobs
Account GetBalance, GetUsage GET /api/v1/chat/credit, GET /api/v1/usage
Files Upload, UploadReader, UploadFile, CreateUploadURL, CommitUpload, ImportLoRA, CreateDownloadURL /common/upload-url, /files/{id}/commit, /files/import, /common/download-url
Asset library CreateAsset, ListAssets, EachAsset, Assets (iterator), GetAsset, UpdateAsset, DeleteAsset, GetAssetUsage, ListCharacters, GetCharacter /api/v1/assets*
Webhooks VerifyWebhook, VerifyEndpointWebhook task callbacks and account endpoints

Models include Kling, Seedance, Wan, Vidu, MiniMax, Nano Banana, GPT Image, Flux, Qwen Image, Seedream, Suno and more across text-to-image, image editing, text-to-video, image-to-video, reference-to-video, video extend, upscaling, text-to-speech, music and speech-to-text. The live list, with each model's input schema and credit price, is ListModels:

models, err := client.ListModels(ctx, popnow.ListModelsOptions{Modality: "video", Task: "image-to-video"})
if err != nil {
	return err
}
for _, m := range models.Items {
	if m.Enabled && m.Available && m.Credits != nil {
		log.Printf("%s  %.1f credits %s", m.Model, m.Credits.PerUnit, m.Credits.Unit)
	}
}

// GetModel always includes the JSON Schema that validates Input.
schema, err := client.GetModel(ctx, "alibaba/wan-3.0/image-to-video")
if err != nil {
	return err
}
log.Println(string(schema.InputSchema))

Show the price first

Quote runs the same validation as CreateTask but creates nothing and holds nothing. Pass its QuoteID and MaxCredits back to make sure the price you showed is the price that is held; a price that moved is refused with code 40901 instead.

request := popnow.CreateTaskInput{
	Model: "alibaba/wan-3.0/image-to-video",
	Input: map[string]any{
		"image_url":        "https://media.popnow.ai/movie/lighthouse.webp",
		"prompt":           "Waves crash against the lighthouse, slow push in",
		"resolution":       "480p",
		"duration_seconds": 5,
	},
}

quote, err := client.Quote(ctx, request)
if err != nil {
	return err
}
log.Printf("at most %d credits ($%s)", quote.MaxCredits, quote.MaxCharge)

request.QuoteID = quote.QuoteID
request.ExpectedCredits = &quote.MaxCredits

key, err := popnow.NewIdempotencyKey() // store it with your job before sending
if err != nil {
	return err
}
task, err := client.CreateTask(ctx, request, key, popnow.CreateTaskOptions{})
if err != nil {
	return err
}
log.Println("accepted", task.TaskID, "holding", task.EstimatedCredits, "credits")

1 credit = $0.01. The hold is the ceiling: unused credits are released when the task settles, and failed or expired tasks release the hold in full.

Skip the first poll

WaitSeconds asks the server to hold the connection open until the task finishes (up to 60 seconds), so a quick model returns its result with the submission.

seconds := 30
task, err := client.CreateTask(ctx, request, key, popnow.CreateTaskOptions{WaitSeconds: &seconds})
if err != nil {
	return err
}

// Task is nil whenever the wait ran out. That is the ordinary outcome, not an
// error: the generation is still running, so poll from here.
final := task.Task
if final == nil {
	final, err = client.WaitForTerminal(ctx, task.TaskID, popnow.WaitOptions{})
	if err != nil {
		return err
	}
}
if final.State != popnow.StateSucceeded {
	log.Println("failed:", final.ErrorCode, final.ErrorMessage)
}

It is an optimisation, never a guarantee: always check task.Task for nil. A terminal state is not necessarily a success, either: failed and expired end the wait too, with ErrorCode set. WaitForTerminal returns *popnow.WaitTimeoutError when its local deadline passes; that is not a verdict, the task is still running, so keep the task ID and check again later.

Start from a local file

Image-to-video, image editing and reference models need your material on PopNow first. UploadFile streams the file (ticket, PUT to storage, commit) and returns a popnow://f/ URI for task input. A public HTTPS URL can be passed directly instead.

uploaded, err := client.UploadFile(ctx, "/path/to/reference.png", "")
if err != nil {
	return err
}
request.Input["image_url"] = uploaded.URI // "popnow://f/fil_..."

// LoRA weights: upload a .safetensors file, or import one from a link.
lora, err := client.ImportLoRA(ctx, popnow.LoRAImport{URL: "owner/repo"})
if err != nil {
	return err
}
log.Println(lora.URI) // goes in loras[].path

Accepted: JPEG, PNG, WebP and GIF images (10 MiB), MP4/WebM video and MP3/WAV audio (90 MiB, 600 s), reference documents (PDF, Word, Excel, PowerPoint, Pages, Numbers, plain text, Markdown) and .safetensors LoRA weights (2 GiB). The content type is inferred from the extension, except Keynote: .key is also how private keys are named, so pass "application/vnd.apple.keynote" yourself.

Keep results in the asset library

Output links expire after about 20 minutes. To keep a result, save it to the asset library: it gets a stable popnow://a/ URI that works in any media field of later requests made with keys of the same workspace.

first := 0
saved, err := client.CreateAsset(ctx, popnow.CreateAssetInput{
	Source: popnow.AssetSourceTaskOutput,
	TaskID: final.TaskID,
	Index:  &first,
	Name:   "Lighthouse clip",
}, "save-"+final.TaskID+"-0")
if err != nil {
	return err
}
log.Println(saved.URI) // "popnow://a/ast_..."

for asset, err := range client.Assets(ctx, popnow.ListAssetsOptions{Kind: "video"}) {
	if err != nil {
		return err
	}
	log.Println(asset.Name, asset.URI)
}

Storage unlocks with your lifetime credit purchases; GetAssetUsage shows the plan and limits. Without a top-up, saving fails with APIError.Reason == "asset_library_locked".

Webhooks instead of polling

Pass CallBackURL (public HTTPS) on CreateTask and PopNow posts the finished task to it. Verify every delivery against the raw body with your webhook signing secret from the API console:

package main

import (
	"io"
	"log"
	"net/http"
	"os"

	popnow "github.com/popnow-ai/popnow-go"
)

func main() {
	secret := os.Getenv("POPNOW_WEBHOOK_SECRET")
	http.HandleFunc("/hooks/popnow", func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(io.LimitReader(r.Body, 10<<20)) // the exact bytes
		if err != nil {
			http.Error(w, "bad body", http.StatusBadRequest)
			return
		}
		delivery, err := popnow.VerifyWebhook(secret, r.Header, body, 0)
		if err != nil {
			http.Error(w, "invalid signature", http.StatusUnauthorized)
			return
		}
		w.WriteHeader(http.StatusOK) // acknowledge within 15 seconds, then do the work
		// DeliveryID is stable across retries: store it to ignore duplicates.
		if delivery.Task != nil {
			log.Println(delivery.DeliveryID, delivery.TaskID, delivery.Task.State)
		}
	})
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Account endpoints registered in the console use the Standard Webhooks format and their own whsec_ secret; verify those with popnow.VerifyEndpointWebhook(secret, r.Header, body, 0), which also accepts the two signatures sent during a secret rotation. Both verifiers check the signature first and the timestamp second (five minutes by default).

Balance, usage and history

All three are scoped to this API key: other keys on the account, and generations started on the website, are not included.

balance, err := client.GetBalance(ctx)
if err != nil {
	return err
}
log.Printf("%d credits available, %d held", balance.AvailableCredits, balance.HeldCredits)

usage, err := client.GetUsage(ctx, popnow.UsageOptions{From: "2026-10-01", To: "2026-10-08"})
if err != nil {
	return err
}
log.Printf("%d calls, %d credits over [%s,%s)", usage.TotalCalls, usage.TotalCredits, usage.From, usage.To)

// Every filter is repeated on every page; only the cursor moves.
for item, err := range client.Tasks(ctx, popnow.ListTasksOptions{From: "2026-10-01", To: "2026-10-08"}) {
	if err != nil {
		return err
	}
	log.Println(item.TaskID, item.State, item.Credits)
}

Dates are UTC calendar dates in a half-open interval [from, to) of at most 92 days. Pin From and To on a walk that can cross midnight UTC: left empty, the server recomputes its window on every page. GetUsage is for reconciliation, not progress: it shares an account-wide limit of 30 requests a minute.

Errors and retries

Every rejected call is a *popnow.APIError:

var apiErr *popnow.APIError
if errors.As(err, &apiErr) {
	log.Println(apiErr.Status, apiErr.Code, apiErr.Reason, apiErr.RequestID, apiErr.RetryAfter)
	if apiErr.Code == popnow.CodeInsufficientCredits {
		log.Println("buy credits at https://popnow.ai/pricing")
	}
}

Branch on Code, never on the message: messages can be translated (Options.AcceptLanguage), codes are not.

Code HTTP Meaning What this client does / what to do
400 400 invalid request or input; msg names the problem fix the input
40003 400 uploaded bytes do not match their ticket upload again with a new ticket
40004 400 valid, but this combination of settings cannot be served now change the named setting
401 401 missing, unknown or revoked key check POPNOW_API_KEY
40201 402 not enough credits for the hold buy credits, resend
40202 402 a key spend cap was reached raise the cap in the console
40301 / 40302 / 40303 403 key not allowed this model / IP not allowed / region not offered adjust the key
40310 403 account email not verified open the verification link
404 404 not found, or another key's resource -
409 / 40901 409 idempotency conflict / quote expired or price changed new key / quote again
429 429 too many requests retried automatically after Retry-After
500 500 server error retried automatically with back-off
503 / 50301 503 dependency or model briefly unavailable retried automatically
50302 503 a synchronous generation failed and was refunded not retried: resend with a new idempotency key

Automatic retries apply only to requests that are safe to repeat: reads, quotes, purges, and submissions that carry an idempotency key. Upload tickets and LoRA imports are never resent. Tune with Options.MaxRetries (-1 disables), RequestTimeout, UploadTimeout and WaitTimeout.

A failed generation shows up as a task with State == "failed" and an ErrorCode such as content_rejected, invalid_asset or timeout, not as an error from WaitForTerminal.

Configuration

Option Default
APIKey POPNOW_API_KEY
BaseURL https://api.popnow.ai/api/v1 (HTTPS only; HTTP allowed for loopback)
HTTPClient &http.Client{} (any Do(*http.Request) works, for proxies or tests)
UserAgent popnow-go/<version> (+https://github.com/popnow-ai/popnow-go)
AcceptLanguage none (English messages)
RequestTimeout / UploadTimeout / WaitTimeout 30 s / 30 min / 10 min
MaxRetries 3

Keep the API key on your server. Never ship it inside a binary you distribute, and never attach it to the signed output links.

FAQ

How do I generate a video from an image with an API in Go?

Upload the image with client.UploadFile (or use a public HTTPS URL), then call client.CreateTask with an image-to-video model such as alibaba/wan-3.0/image-to-video and Input containing image_url, prompt, resolution and duration_seconds. Wait with client.WaitForTerminal and read ReadyAssets()[0].URL.

How much does a PopNow API call cost?

Each model has a credit price (1 credit = $0.01), shown by ListModels in Model.Credits and on popnow.ai/pricing. client.Quote returns the exact MaxCredits for one request without spending anything. API calls and the website share one wallet and one price list. Failed tasks are refunded.

Which AI models can I call from Go?

Every image, video and audio model on PopNow: Kling, Seedance, Wan, Vidu, MiniMax, Nano Banana, GPT Image, Flux, Qwen Image, Seedream, Suno and more. client.ListModels(ctx, popnow.ListModelsOptions{}) returns the live list with input schemas.

How do I avoid paying twice when a request times out?

Pass the same idempotency key on every resend of the same piece of work. For 24 hours a repeat returns the original task instead of creating a new one. CreateTask requires a key for this reason, and the client's own automatic retries reuse it.

Do I have to poll, or can PopNow call my server?

Either. Set CallBackURL on CreateTask and verify each delivery with popnow.VerifyWebhook; or set CreateTaskOptions.WaitSeconds to have the server hold the request open for quick models; or poll with WaitForTerminal, which backs off for you.

How long are generated files kept?

Signed output links last about 20 minutes; read the task again or call CreateDownloadURL for a fresh one. Outputs themselves follow the retention policy shown in Task.Retention (you can shorten it per task with CreateTaskOptions.RetentionSeconds). To keep a result indefinitely, save it with CreateAsset.

Is there an MCP server for ChatGPT or Claude?

Yes: a remote MCP server at https://api.popnow.ai/mcp exposes image, video, music and speech tools. See popnow.ai/mcp and popnow-mcp.

Development

gofmt -l .
go vet ./...
go test -race ./...

The tests use local fake servers and need no API key. TestContractHasNotDrifted compares contracts/openapi.yaml with the published spec (set POPNOW_SKIP_LIVE_CONTRACT=1 offline), every Go block in this README is compiled by readme_test.go, and POPNOW_LIVE_TESTS=1 runs one request against the real API with a fake key to check the 401 envelope.

Contributing

Issues and pull requests are welcome; see CONTRIBUTING.md. To report a vulnerability, see SECURITY.md.

License

MIT © PopNow

Documentation

Overview

Package popnow is the Go client for the PopNow API.

PopNow is a browser-based AI studio for videos, images and music. Its developer API serves the same image, video and audio models, prices and credit wallet as https://popnow.ai. This package covers the asynchronous media workflow end to end: discover a model and its input schema, upload local reference material, quote a request, submit it, wait for a terminal state, collect the result, keep it in the asset library, and verify the webhook that announces it.

It depends on the standard library only.

Two things decide most of the code in this package:

  • Every /api/v1 response is the {code,msg,data,request_id} envelope, and a successful HTTP status does not mean the call succeeded. Branch on the business code, never on the message text.
  • HTTP 202 from task creation means accepted, not finished. Success or failure is reported by the task's state, read back with GetTask.

Billing: 1 credit = $0.01. Amounts are reported both as integer credits (MaxCredits, EstimatedCredits, Credits, ...) and as the equivalent USD decimal string (MaxCharge, EstimatedCost, Cost, ...).

The contract this package is written against is published at https://popnow.ai/docs/openapi.yaml.

Index

Constants

View Source
const (
	AssetSourceTaskOutput = "task_output"
	AssetSourceUpload     = "upload"
	AssetSourceURL        = "url"
)

Asset sources accepted by CreateAsset and reported on Asset.Source.

View Source
const (
	// APIBaseURL is the production API root for the native endpoints.
	APIBaseURL = "https://api.popnow.ai/api/v1"

	// APIKeyEnv is the environment variable New reads when Options.APIKey is
	// empty.
	APIKeyEnv = "POPNOW_API_KEY"

	// DefaultRequestTimeout bounds one JSON API attempt, not the whole call.
	// Give the context its own deadline to bound a call including retries.
	DefaultRequestTimeout = 30 * time.Second

	// DefaultUploadTimeout bounds the PUT that streams bytes into object
	// storage, and the server-side download behind ImportLoRA. It is far
	// longer than DefaultRequestTimeout: audio and video may be 90 MiB and
	// LoRA weights 2 GiB, and a 30 second cut-off would read like a network
	// fault while really being this client hanging up on itself.
	DefaultUploadTimeout = 30 * time.Minute

	// DefaultWaitTimeout bounds WaitForTerminal. It is a local safety bound,
	// not a service level: reaching it says nothing about the remote task.
	DefaultWaitTimeout = 10 * time.Minute

	// DefaultPollInterval is the first gap between task polls; the gap grows
	// by half on every pass and stops at MaxPollInterval.
	DefaultPollInterval = 2 * time.Second
	// MaxPollInterval caps the growth of the polling gap.
	MaxPollInterval = 15 * time.Second

	// DefaultMaxRetries is how many extra attempts a replayable request gets.
	DefaultMaxRetries = 3

	// WebhookTolerance is the default clock skew the webhook verifiers accept.
	WebhookTolerance = 5 * time.Minute
)

Defaults for a new Client. Every one of them can be overridden in Options.

View Source
const (
	StateQueued    = "queued"
	StateRunning   = "running"
	StateSucceeded = "succeeded"
	StateFailed    = "failed"
	StateCanceled  = "canceled"
	StateExpired   = "expired"
)

Task states. The two in-flight states are the stable part of this set: treat anything else as terminal rather than listing the terminal values, so that a state added later ends a wait instead of extending it forever. StateCanceled exists only on old records.

View Source
const (
	ErrorCodeInvalidRequest         = "invalid_request"
	ErrorCodeUnsupportedCombination = "unsupported_combination"
	ErrorCodeContentRejected        = "content_rejected"
	ErrorCodeRateLimited            = "rate_limited"
	ErrorCodeUpstreamUnavailable    = "upstream_unavailable"
	ErrorCodeGenerationFailed       = "generation_failed"
	ErrorCodeTimeout                = "timeout"
	ErrorCodeInvalidAsset           = "invalid_asset"
	ErrorCodeUpstreamFailed         = "upstream_failed"
)

Failure identifiers reported in Task.ErrorCode. The set is closed; handle any value you do not recognise the way you handle ErrorCodeUpstreamFailed.

View Source
const (
	CodeSuccess                = 200
	CodeBadRequest             = 400
	CodeUploadMismatch         = 40003
	CodeUnsupportedCombination = 40004
	CodeUnauthorized           = 401
	CodeInsufficientCredits    = 40201
	CodeSpendCapReached        = 40202
	CodeForbidden              = 403
	CodeModelNotAllowed        = 40301
	CodeIPNotAllowed           = 40302
	CodeRegionUnavailable      = 40303
	CodeEmailNotVerified       = 40310
	CodeNotFound               = 404
	CodeConflict               = 409
	CodeQuoteRejected          = 40901
	CodePayloadTooLarge        = 413
	CodeRateLimited            = 429
	CodeServerError            = 500
	CodeDependencyUnavailable  = 503
	CodeModelUnavailable       = 50301
	CodeGenerationRefunded     = 50302
)

Business codes. They travel in the envelope's code field and are the thing to branch on: several of them share one HTTP status.

40003  the uploaded bytes do not match their ticket (size, media type or
       signature). Not retryable: request a fresh ticket with the real
       content type and size and upload again.
40004  the request is valid but no deployment serves this exact
       combination of settings right now. Not retryable: change the
       parameter named in the message, or pick another model.
40201  not enough credits for this task's hold. Buy credits, then resend.
40202  a key spend cap was reached.
40301  this key may not call this model.
40302  the caller's address is outside this key's IP allow-list.
40303  the service is not offered in the caller's region.
40310  the account has not verified its email address yet. The same key
       works as soon as the address is verified.
40901  the quote expired, or the request or price changed since it was
       signed. Quote again, then resubmit.
503    a supporting dependency is briefly unavailable. Retryable; honour
       Retry-After when the response carries it.
50301  the model has no usable deployment or effective price right now.
50302  a synchronous generation failed and the charge was refunded.
       Resend under a new idempotency key: the old key replays the
       recorded failure. This client therefore never resends it for you.

503, 50301 and 50302 share HTTP 503 and mean three different things, which is why this client reads the business code first and the status second.

View Source
const ContentTypeLoRA = "application/octet-stream"

ContentTypeLoRA is the media type LoRA weights (.safetensors) upload under.

View Source
const UserAgent = "popnow-go/" + Version + " (+https://github.com/popnow-ai/popnow-go)"

UserAgent identifies this client on every API request.

It is always sent explicitly. Without it net/http sends Go-http-client/1.1, which says nothing beyond "a Go program", and the API edge refuses some generic default client agents outright. A versioned agent also makes a bad release or a version-specific bug visible in request logs.

View Source
const Version = "0.1.0"

Version is this package's released version, and the only place that number is written down. UserAgent derives from it, so bumping it here is the whole release edit.

Keep it equal to the git tag it ships under, without the leading v. A tagged module exposes its version only to the programs that depend on it, never to itself, so nothing can read it back at run time.

Variables

View Source
var (
	ErrWebhookRejected  = errors.New("popnow: webhook rejected")
	ErrWebhookStale     = fmt.Errorf("%w: timestamp outside the tolerance window", ErrWebhookRejected)
	ErrWebhookSignature = fmt.Errorf("%w: signature mismatch", ErrWebhookRejected)
)

Webhook rejection reasons. Every one of them wraps ErrWebhookRejected, so a handler that only wants to answer 401 can test for that alone.

Functions

func NewIdempotencyKey

func NewIdempotencyKey() (string, error)

NewIdempotencyKey mints a key with enough entropy that two submissions never collide. Store it next to your own record of the job before you send it: the point of the key is to be available again after the crash that lost the response. A key derived from your own data ("order-8814-render-1") works just as well and can be recomputed instead of stored.

Types

type APIError

type APIError struct {
	// Status is the HTTP status. It is 0 when no response was parsed.
	Status int
	// Code is the business code from the envelope, 0 when the body was not an
	// envelope. Branch on this, not on Message.
	Code int
	// Message is the server's explanation, in English unless the account or
	// the Accept-Language header selects another language. Never parse it.
	Message string
	// Reason is the machine-readable data.reason some failures carry, for
	// example "asset_library_locked" or "asset_daily_limit". Empty when the
	// failure has none. New values may appear; treat unknown ones as empty.
	Reason string
	// RequestID correlates this call with the server's logs.
	RequestID string
	// RetryAfter is the server's own backoff request, 0 when absent.
	RetryAfter time.Duration
}

APIError is a rejected call: an HTTP status, the business code to branch on, and the request ID to quote to support. Recover it with errors.As.

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Refunded

func (e *APIError) Refunded() bool

Refunded reports code 50302: generation failed and the hold was released, so this cost nothing. Submit the same input again under a new idempotency key.

func (*APIError) Retryable

func (e *APIError) Retryable() bool

Retryable reports whether sending the identical request again is worth it.

type Asset

type Asset struct {
	ID string `json:"id"`
	// URI is the stable popnow://a/ast_... reference. It works in any media
	// field of a model input, for keys of the same workspace, and is never a
	// downloadable link. Persist this, not PreviewURL.
	URI         string `json:"uri"`
	Name        string `json:"name"`
	Kind        string `json:"kind"`
	ContentType string `json:"contentType"`
	Bytes       int64  `json:"bytes"`
	// Width, Height and DurationSeconds are measured by the server, 0 when
	// they do not apply.
	Width           int     `json:"width,omitempty"`
	Height          int     `json:"height,omitempty"`
	DurationSeconds float64 `json:"durationSeconds,omitempty"`
	// Source is task_output, upload or url; SourceID is your task or upload
	// id, or an opaque digest of the address for url.
	Source    string    `json:"source"`
	SourceID  string    `json:"sourceId"`
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
	// PreviewURL is a private preview signed for at most 20 minutes, empty
	// when signing fails. Do not store or share it.
	PreviewURL          string     `json:"previewUrl"`
	PreviewURLExpiresAt *time.Time `json:"previewUrlExpiresAt,omitempty"`
	// Favorite belongs to the library, so everyone in a team sees the same.
	Favorite bool `json:"favorite"`
	// Model and ModelName name the model that generated the asset; empty for
	// uploads and links.
	Model     string `json:"model"`
	ModelName string `json:"modelName"`
}

Asset is one file in the long-term private asset library.

The library is decided by the key: a personal key reaches your personal library, a team workspace key reaches that team's shared library. Saved files are kept until they are deleted.

type AssetDeleted

type AssetDeleted struct {
	ID      string `json:"id"`
	Deleted bool   `json:"deleted"`
}

AssetDeleted confirms DeleteAsset.

type AssetLayer

type AssetLayer struct {
	ZIndex int `json:"zIndex"`
	// Name is the model's own label for the layer, when it gives one.
	Name string `json:"name,omitempty"`
	// BoundingBox is [x1, y1, x2, y2] in base-image pixels. It is nil on the
	// base image and on models that do not report positions.
	BoundingBox []int `json:"boundingBox,omitempty"`
}

AssetLayer places one output of a layer-decomposition task.

The first asset is the base image (ZIndex 0) and every other asset is one transparent PNG layer. Stack them by ascending ZIndex to rebuild the picture, rather than relying on the order of Assets.

type AssetList

type AssetList struct {
	Items []Asset `json:"items"`
	// NextCursor is absent on the last page.
	NextCursor string `json:"nextCursor,omitempty"`
}

AssetList is one page of the asset library.

type AssetPlan

type AssetPlan struct {
	ID           string `json:"id"`
	Name         string `json:"name"`
	MinTopupUsd  string `json:"minTopupUsd"`
	MaxBytes     int64  `json:"maxBytes"`
	MaxCount     int64  `json:"maxCount"`
	MaxFileBytes int64  `json:"maxFileBytes"`
}

AssetPlan is one storage plan of the asset library. Byte values use binary units.

type AssetQuota

type AssetQuota struct {
	Tier  AssetPlan `json:"tier"`
	Usage struct {
		Count int64 `json:"count"`
		Bytes int64 `json:"bytes"`
	} `json:"usage"`
	Limits struct {
		Tier           string `json:"tier"`
		MaxCount       int64  `json:"maxCount"`
		MaxBytes       int64  `json:"maxBytes"`
		MaxFileBytes   int64  `json:"maxFileBytes"`
		GlobalMaxBytes int64  `json:"globalMaxBytes"`
		NameMaxChars   int    `json:"nameMaxChars"`
	} `json:"limits"`
	// Full is true when no new asset fits.
	Full bool `json:"full"`
	// NextTier is nil on the highest plan.
	NextTier *AssetPlan `json:"nextTier"`
	// TopupToNextTierUsd and LifetimeTopupUsd are empty when Scope.CanTopUp
	// is false.
	TopupToNextTierUsd string      `json:"topupToNextTierUsd,omitempty"`
	LifetimeTopupUsd   string      `json:"lifetimeTopupUsd,omitempty"`
	Tiers              []AssetPlan `json:"tiers"`
	DailyCreateLimit   int64       `json:"dailyCreateLimit"`
	CreatedToday       int64       `json:"createdToday"`
	Scope              AssetScope  `json:"scope"`
}

AssetQuota is the asset library plan, usage and limits.

type AssetScope

type AssetScope struct {
	WorkspaceID string `json:"workspaceId"`
	Personal    bool   `json:"personal"`
	Role        string `json:"role"`
	CanWrite    bool   `json:"canWrite"`
	CanTopUp    bool   `json:"canTopUp"`
}

AssetScope says which library the key reaches and what it may do there.

type Balance

type Balance struct {
	// AvailableCredits is what a new task can draw on (rounded down to a whole
	// credit), HeldCredits what accepted tasks have already reserved, and
	// TotalCredits the two together.
	AvailableCredits int64 `json:"availableCredits"`
	HeldCredits      int64 `json:"heldCredits"`
	TotalCredits     int64 `json:"totalCredits"`
	// Available, Held and Total are the same amounts in USD.
	Available string `json:"available"`
	Held      string `json:"held"`
	Total     string `json:"total"`
	// Funding breaks the balance down by where the money came from. It is
	// absent on accounts with nothing but a plain wallet.
	Funding *FundingOverview `json:"funding,omitempty"`
}

Balance is what this account can spend right now, in credits and in USD decimal strings. Amounts are never floats: cents do not survive binary floating point.

type Character

type Character struct {
	ID           string `json:"id"`
	Kind         string `json:"kind"`
	Name         string `json:"name"`
	Note         string `json:"note"`
	CoverAssetID string `json:"coverAssetId"`
	// Cover is nil while the character has no images.
	Cover *Asset `json:"cover"`
	// Assets are the character's images in order.
	Assets    []Asset   `json:"assets"`
	Count     int       `json:"count"`
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
}

Character is a named, ordered set of images in the asset library. It references those assets instead of copying them. Characters are created and edited on popnow.ai; the API only reads them.

type CharacterList

type CharacterList struct {
	Items []Character `json:"items"`
	// NextCursor is absent on the last page.
	NextCursor string `json:"nextCursor,omitempty"`
}

CharacterList is one page of characters.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client calls the PopNow API. It is safe for concurrent use.

func New

func New(opts Options) (*Client, error)

New builds a client. It fails when no API key is available, because every endpoint needs one and discovering that at the first call is worse.

func (*Client) Assets

func (c *Client) Assets(ctx context.Context, opts ListAssetsOptions) iter.Seq2[Asset, error]

Assets is EachAsset as a range-over-func iterator. A failed page yields one final (zero, err) pair and ends the sequence.

func (*Client) CommitUpload

func (c *Client) CommitUpload(ctx context.Context, fileID string) (*UploadedFile, error)

CommitUpload verifies the bytes against the ticket (size, media type, signature), copies them to an immutable private object and returns the popnow:// URI to put in task input. Repeating a successful commit is idempotent.

Code 40003 here means the stored file does not match what the ticket was issued for. Do not retry it: ask for a new ticket and upload again.

func (*Client) CreateAsset

func (c *Client) CreateAsset(ctx context.Context, input CreateAssetInput, idempotencyKey string) (*Asset, error)

CreateAsset copies a file into the key's asset library and returns it with its stable popnow://a/ URI. Saving the same source again returns the existing asset without using more space.

idempotencyKey is required by the contract and bound to the request for 24 hours. Storage unlocks with the owner's lifetime top-ups: without one this returns 400 with APIError.Reason "asset_library_locked"; see GetAssetUsage.

func (*Client) CreateDownloadURL

func (c *Client) CreateDownloadURL(ctx context.Context, taskID, key string) (*DownloadTicket, error)

CreateDownloadURL issues a fresh signed link to one output of a finished task. A finished task already carries usable links, so this is only needed once those have expired; polling the task again does the same job.

Leave key empty to select the first output. Do not attach your API key when fetching the link: it is a bearer grant on its own.

func (*Client) CreateTask

func (c *Client) CreateTask(
	ctx context.Context,
	input CreateTaskInput,
	idempotencyKey string,
	opts CreateTaskOptions,
) (*CreateTaskResult, error)

CreateTask submits a generation and returns as soon as it is accepted.

Acceptance is not completion. The returned state is queued; read the outcome back with GetTask or WaitForTerminal. EstimatedCredits is held, not charged, and it is also the ceiling: the settled charge can only be lower.

idempotencyKey is required by this client even though the contract allows it to be omitted, because the automatic resend on a network fault is only sound when it exists. Without a key, a request that times out leaves you unable to tell an accepted task from a lost one, and the obvious reaction - send it again - is a second generation and a second charge. Reuse the same value for every attempt at the same logical submission: for 24 hours, and for this API key, a repeat returns the original task instead of creating another.

func (*Client) CreateUploadURL

func (c *Client) CreateUploadURL(ctx context.Context, contentType string, size int64) (*UploadTicket, error)

CreateUploadURL asks for a presigned slot for exactly one file of exactly the declared size. Images are capped at 10 MiB, audio, video and documents at 90 MiB (audio and video at 600 seconds), LoRA weights at 2 GiB; the ticket's MaxBytes is the authoritative ceiling.

func (*Client) DeleteAsset

func (c *Client) DeleteAsset(ctx context.Context, assetID string) (*AssetDeleted, error)

DeleteAsset removes an asset from the library and every collection; its URI stops working in new requests. Deleting an unknown or already deleted asset returns the same result, so retries are safe.

func (*Client) EachAsset

func (c *Client) EachAsset(ctx context.Context, opts ListAssetsOptions, fn func(Asset) bool) error

EachAsset walks every page of ListAssets and calls fn once per asset, repeating every filter and moving only the cursor. Return false from fn to stop early.

func (*Client) EachTask

func (c *Client) EachTask(ctx context.Context, opts ListTasksOptions, fn func(TaskListItem) bool) error

EachTask walks every page of ListTasks and calls fn once per task. Return false from fn to stop early; the error is the first page that failed.

Every filter is repeated unchanged on each request and only the cursor moves: a cursor is only meaningful against the query it was issued for.

Pin From and To yourself for any walk that might cross midnight UTC. With them empty the server recomputes its default window on every page, so a walk that starts at 23:59 and continues at 00:01 splices two different answers together and nothing reports it.

func (*Client) GetAsset

func (c *Client) GetAsset(ctx context.Context, assetID string) (*Asset, error)

GetAsset reads one asset with a fresh private preview. An unknown asset and one this key's library does not hold return the same 404.

func (*Client) GetAssetUsage

func (c *Client) GetAssetUsage(ctx context.Context) (*AssetQuota, error)

GetAssetUsage reports the asset library plan, how much is stored, its limits and what the next plan unlocks.

func (*Client) GetBalance

func (c *Client) GetBalance(ctx context.Context) (*Balance, error)

GetBalance reads the available, held and total balance of this account. API calls and generations on popnow.ai share this one wallet.

func (*Client) GetCharacter

func (c *Client) GetCharacter(ctx context.Context, characterID string) (*Character, error)

GetCharacter reads one character with its images in order, each with a fresh private preview.

func (*Client) GetModel

func (c *Client) GetModel(ctx context.Context, model string) (*Model, error)

GetModel reads one model, always including its input schema.

A model identifier contains slashes, so it is escaped into a single path segment here; sending them raw would address a different resource.

func (*Client) GetTask

func (c *Client) GetTask(ctx context.Context, taskID string) (*Task, error)

GetTask reads one task created by this API key. An unknown task, another account's task and a sibling key's task are all a 404; the three cases are deliberately indistinguishable.

func (*Client) GetUsage

func (c *Client) GetUsage(ctx context.Context, opts UsageOptions) (*UsageReport, error)

GetUsage reports this API key's settled spend and call counts.

Numbers are attributed to when a task was created, and settlement can land late, so a figure read today can still change tomorrow. This endpoint sits behind an account-wide bucket of thirty requests a minute shared by every key, and that limiter fails closed: follow tasks with GetTask or WaitForTerminal, never by polling usage.

func (*Client) ImportLoRA

func (c *Client) ImportLoRA(ctx context.Context, input LoRAImport) (*UploadedFile, error)

ImportLoRA has the service download a .safetensors LoRA file once and store it as your upload, so weights behind a sign-in can be used without making them public. Put the returned URI in loras[].path.

The server downloads the whole file inside this call, so it gets the upload timeout rather than the ordinary request budget. It is not retried automatically: every successful call stores a new copy.

func (*Client) ListAssets

func (c *Client) ListAssets(ctx context.Context, opts ListAssetsOptions) (*AssetList, error)

ListAssets returns one page of the key's asset library. Listing and reading assets is limited to 60 requests per minute per account.

func (*Client) ListCharacters

func (c *Client) ListCharacters(ctx context.Context, opts ListCharactersOptions) (*CharacterList, error)

ListCharacters returns one page of the characters in the key's asset library, newest first.

func (*Client) ListModels

func (c *Client) ListModels(ctx context.Context, opts ListModelsOptions) (*ModelList, error)

ListModels returns every image, video and audio model this API key may call, with its price in credits. The list is not paginated.

func (*Client) ListTasks

func (c *Client) ListTasks(ctx context.Context, opts ListTasksOptions) (*TaskList, error)

ListTasks returns one page of this API key's task history, newest first.

The scope is this API key alone: tasks of sibling keys and generations started on the website are not here. Pages read live state, not a frozen snapshot.

func (*Client) PurgeTask

func (c *Client) PurgeTask(ctx context.Context, taskID string) (*PurgeResult, error)

PurgeTask destroys a finished task's stored content: the generated objects, the result payload and the request text including the prompt. The spend record stays.

The task ID is the idempotency key, so a repeat after a timeout is safe and returns the original PurgedAt. A queued or running task returns 400.

func (*Client) Quote

func (c *Client) Quote(ctx context.Context, input CreateTaskInput) (*Quote, error)

Quote prices one exact request for five minutes without creating a task, holding credits or starting anything. It runs the same validation and admission rules as CreateTask, so it is also the cheapest way to find out that an input is wrong.

Pass the returned QuoteID and MaxCredits to CreateTask to refuse a price that moved in the meantime.

func (*Client) RetryTask

func (c *Client) RetryTask(ctx context.Context, taskID, idempotencyKey string) (*RetryTaskResult, error)

RetryTask creates a new task from a failed or expired one. The source task is never modified, and the current schema, price, permissions and balance are all evaluated again, so a retry can cost a different amount or be refused.

The source must be terminal and settled; retrying too early returns 409, and retrying a succeeded task returns 404. Retention is inherited from the source. The idempotency key is scoped to the source task.

func (*Client) Tasks

Tasks is EachTask as a range-over-func iterator:

for task, err := range client.Tasks(ctx, popnow.ListTasksOptions{}) {
	if err != nil {
		return err
	}
	log.Println(task.TaskID)
}

A failed page yields one final (zero, err) pair and ends the sequence.

func (*Client) UpdateAsset

func (c *Client) UpdateAsset(ctx context.Context, assetID string, input UpdateAssetInput) (*Asset, error)

UpdateAsset renames an asset or changes its favorite mark. The URI stays the same and nothing is copied. Repeating the same request is safe.

func (*Client) Upload

func (c *Client) Upload(ctx context.Context, data []byte, contentType string) (*UploadedFile, error)

Upload runs all three steps for bytes already in memory: take a ticket, PUT the bytes straight into object storage, then commit. Only the URI on the returned file belongs in task input.

Uploading is free; you pay for the task that consumes the file. A public HTTPS URL can be passed in the input field directly and skips all of this.

func (*Client) UploadFile

func (c *Client) UploadFile(ctx context.Context, path, contentType string) (*UploadedFile, error)

UploadFile streams a local file. An empty contentType is inferred from the extension, which only works for the accepted media types (.safetensors becomes ContentTypeLoRA).

func (*Client) UploadReader

func (c *Client) UploadReader(ctx context.Context, r io.Reader, size int64, contentType string) (*UploadedFile, error)

UploadReader is Upload for a stream of a known size, so a large video or a LoRA file never has to sit in memory. r must yield exactly size bytes.

func (*Client) WaitForTerminal

func (c *Client) WaitForTerminal(ctx context.Context, taskID string, opts WaitOptions) (*Task, error)

WaitForTerminal polls a task until it stops being queued or running, backing off between reads.

Terminal is defined as "neither of the two in-flight states" rather than as a list of the known endings, so a state added later ends the wait instead of extending it forever. Only "succeeded" carries a result.

A local timeout is not a verdict: it returns *WaitTimeoutError, and the generation is still running. Keep the task ID and reconcile it later, or use a callback URL instead of waiting.

type CreateAssetInput

type CreateAssetInput struct {
	Source string `json:"source"`
	TaskID string `json:"taskId,omitempty"`
	Key    string `json:"key,omitempty"`
	// Index is a pointer because 0, the first output, is a real value.
	Index  *int   `json:"index,omitempty"`
	FileID string `json:"fileId,omitempty"`
	URL    string `json:"url,omitempty"`
	// Name defaults to "Saved asset" on the server; at most 80 characters.
	Name string `json:"name,omitempty"`
}

CreateAssetInput chooses what to save. Set Source and the fields that go with it:

  • AssetSourceTaskOutput: TaskID and either Key or Index (the zero-based position in output.assets). The task must be one of your own successful tasks.
  • AssetSourceUpload: FileID of a committed upload (fil_... or popnow://f/fil_...).
  • AssetSourceURL: URL, a public http or https address on port 80 or 443 without credentials. Only JPEG, PNG, WebP and GIF images, MP4 and WebM video and MP3, WAV, FLAC and Ogg audio are accepted.

type CreateTaskInput

type CreateTaskInput struct {
	// Model is an exact call name from the catalogue, in
	// <publisher>/<model>/<task> form, for example
	// "alibaba/z-image-turbo/text-to-image".
	Model string `json:"model"`
	// Input is validated against that model's input schema. Media fields take
	// a public HTTPS URL, a committed popnow://f/ upload URI, a saved
	// popnow://a/ asset URI, or a small Base64 image data URI.
	Input map[string]any `json:"input"`
	// CallBackURL is an optional public HTTPS endpoint for terminal delivery.
	// Verify every delivery with VerifyWebhook before acting on it.
	CallBackURL string `json:"callBackUrl,omitempty"`
	// QuoteID together with ExpectedCredits (or ExpectedCost) confirms a
	// price: if the price moved since the quote was signed, the task is
	// rejected with code 40901 instead of being accepted at the new amount.
	QuoteID string `json:"quoteId,omitempty"`
	// ExpectedCredits is the confirmed number of credits to hold, as returned
	// in Quote.MaxCredits. It is a pointer because 0 is a real amount. When
	// both are set, ExpectedCredits wins over ExpectedCost.
	ExpectedCredits *int64 `json:"expectedCredits,omitempty"`
	// ExpectedCost is the same confirmation as a USD decimal string.
	ExpectedCost string `json:"expectedCost,omitempty"`
}

CreateTaskInput is the body shared by Quote and CreateTask.

type CreateTaskOptions

type CreateTaskOptions struct {
	// RetentionSeconds, when non-nil, shortens how long this task's outputs
	// and its stored request text are kept (sent as X-PopNow-Retention). It
	// can only shorten: the effective value is the smallest of this, the
	// account setting and the platform maximum, and it is reported back in
	// Task.Retention.
	//
	// It is a pointer because 0 is a meaningful value - the outputs go as
	// soon as the task reaches a terminal state - so it must not be what an
	// empty options struct sends.
	RetentionSeconds *int

	// WaitSeconds, when non-nil, asks the service to hold the connection open
	// for that many seconds waiting for the task to finish, so the common case
	// needs no polling loop. Finishing inside the budget fills
	// CreateTaskResult.Task with the complete record; running out of it
	// returns the ordinary acceptance and you poll from there.
	//
	// It is an optimisation, not a guarantee. The value is sent exactly as
	// given: the service clamps anything above its ceiling (60 today) and
	// ignores, rather than rejects, a value that is not a positive integer.
	// No ceiling is enforced here, because a copy of the platform's number is
	// a constant that can go stale without anything saying so.
	//
	// Setting it also lifts this one request's own deadline to cover the wait.
	WaitSeconds *int
}

CreateTaskOptions carries the optional header and query that accompany a submission.

type CreateTaskResult

type CreateTaskResult struct {
	TaskID string `json:"taskId"`
	// State is queued for a new submission. An idempotent replay reports the
	// original task's current state instead, which may already be terminal.
	State string `json:"state"`
	// EstimatedCredits is the number of credits held, and the ceiling on the
	// final charge. EstimatedCost is the same amount in USD. Unused credits
	// are released when the task settles.
	//
	// Both are zero when Task is non-nil: a task that already finished has a
	// real cost instead of an estimate, in Task.Credits and Task.Cost.
	EstimatedCredits int64  `json:"estimatedCredits"`
	EstimatedCost    string `json:"estimatedCost"`
	// DeadlineAt is the server's execution deadline, fixed at acceptance. It
	// is not a local wait bound and not an artefact expiry.
	DeadlineAt time.Time `json:"deadlineAt"`

	// Task is the complete terminal record, non-nil only when
	// CreateTaskOptions.WaitSeconds was set and the task finished inside that
	// budget. It is exactly what GetTask would return.
	//
	// Nil is the ordinary outcome and is not a failure: the wait ran out, the
	// task is still running, and you poll from here. Code that reads Task
	// without checking for nil works right up until the first slow generation.
	Task *Task `json:"-"`
}

CreateTaskResult is the acceptance record, not a result.

func (*CreateTaskResult) InFlight

func (r *CreateTaskResult) InFlight() bool

InFlight reports whether the task was still queued or running when the service produced this answer. Everything else is terminal, including a state this client has never heard of.

type CreditFacility

type CreditFacility struct {
	Enabled      bool       `json:"enabled"`
	LimitUsd     string     `json:"limitUsd"`
	AvailableUsd string     `json:"availableUsd"`
	UsedUsd      string     `json:"usedUsd"`
	HeldUsd      string     `json:"heldUsd"`
	ExpiresAt    *time.Time `json:"expiresAt,omitempty"`
	Status       string     `json:"status"`
	Version      int64      `json:"version"`
}

CreditFacility is approved pay-later capacity. LimitUsd is a ceiling, not funds.

type DownloadTicket

type DownloadTicket struct {
	Key       string    `json:"key"`
	URL       string    `json:"url"`
	ExpiresAt time.Time `json:"expiresAt"`
}

DownloadTicket is a fresh signed link to one output of a finished task.

type EndpointEvent

type EndpointEvent struct {
	// ID is the webhook-id header. It stays the same across retries: use it
	// to ignore duplicates.
	ID string `json:"-"`
	// Type is task.completed, task.succeeded, task.failed, task.expired or
	// task.canceled. Treat the set as open.
	Type string `json:"type"`
	Data struct {
		// Object is the task record at the moment the event was created.
		Object *Task `json:"object"`
	} `json:"data"`
	// Raw is the verified body, for fields this struct does not name.
	Raw json.RawMessage `json:"-"`
}

EndpointEvent is a verified account endpoint delivery.

func VerifyEndpointWebhook

func VerifyEndpointWebhook(secret string, header http.Header, rawBody []byte, tolerance time.Duration) (*EndpointEvent, error)

VerifyEndpointWebhook authenticates one delivery to an account endpoint registered in the API console. These follow the Standard Webhooks format:

webhook-signature: v1,base64( HMAC-SHA256( key, "<webhook-id>.<webhook-timestamp>.<rawBody>" ) )

Two differences from VerifyWebhook: the raw body itself is signed (no SHA-256 step), and the HMAC key is the Base64-decoded part of the endpoint's whsec_ secret, not the secret string. During a secret rotation the header can hold several space-separated signatures; any match is accepted.

tolerance bounds the accepted clock skew; pass 0 for WebhookTolerance.

type FundingGrant

type FundingGrant struct {
	ID           string     `json:"id"`
	Name         string     `json:"name"`
	AmountUsd    string     `json:"amountUsd"`
	AvailableUsd string     `json:"availableUsd"`
	HeldUsd      string     `json:"heldUsd"`
	SpentUsd     string     `json:"spentUsd"`
	Status       string     `json:"status"`
	StartsAt     time.Time  `json:"startsAt"`
	ExpiresAt    *time.Time `json:"expiresAt,omitempty"`
	// ModelSlugs restricts this grant. Empty means every model, not none.
	ModelSlugs   []string  `json:"modelSlugs,omitempty"`
	CustomerMemo string    `json:"customerMemo"`
	CreatedAt    time.Time `json:"createdAt"`
}

FundingGrant is one promotional grant.

type FundingOverview

type FundingOverview struct {
	// BalanceUsd is the net wallet balance. It can be negative.
	BalanceUsd string `json:"balanceUsd"`
	HeldUsd    string `json:"heldUsd"`
	// PrepaidAvailableUsd is unrestricted; GrantAvailableUsd may not pay for
	// every model, which is decided when a task is admitted.
	PrepaidAvailableUsd string `json:"prepaidAvailableUsd"`
	GrantAvailableUsd   string `json:"grantAvailableUsd"`
	// CashShortfallUsd is a payment recovery gap. Non-zero blocks new tasks.
	CashShortfallUsd string          `json:"cashShortfallUsd"`
	Credit           *CreditFacility `json:"credit,omitempty"`
	Grants           []FundingGrant  `json:"grants,omitempty"`
	// GrantsHasMore says Grants was truncated.
	GrantsHasMore bool `json:"grantsHasMore"`
}

FundingOverview separates the sources behind a balance, because they do not behave alike: a grant can be restricted to certain models and credit is an approved ceiling rather than money on hand.

type HTTPDoer

type HTTPDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

HTTPDoer is the slice of *http.Client this package needs, so that tests can substitute a transport without a network.

type ListAssetsOptions

type ListAssetsOptions struct {
	// Kind is image, video or audio.
	Kind string
	// Sources limits the list to task_output, upload and/or url.
	Sources []string
	// Q is a case-insensitive substring of the asset name.
	Q string
	// Favorite, when non-nil, keeps only favorites (true) or only the rest.
	Favorite *bool
	// CollectionID keeps one collection of the same library.
	CollectionID string
	// EntityID keeps the images of one character.
	EntityID string
	// Sort is recent (the default), oldest, name or size. A cursor only works
	// with the Sort it came from.
	Sort string
	// Cursor is the NextCursor of the previous page.
	Cursor string
	// Limit is the page size, 1 to 100.
	Limit int
}

ListAssetsOptions filters and orders the asset library. A zero field means "do not filter".

type ListCharactersOptions

type ListCharactersOptions struct {
	// AssetID keeps only the characters that contain this asset.
	AssetID string
	Cursor  string
	// Limit is the page size, 1 to 100.
	Limit int
}

ListCharactersOptions filters the character list.

type ListModelsOptions

type ListModelsOptions struct {
	// Modality is image, video or audio.
	Modality string
	// Provider is the model's creator.
	Provider string
	// Task is an exact task, for example text-to-image or image-to-video.
	// "edit" and "image-to-image" select the same editing endpoints.
	Task string
	// Search matches names and descriptions.
	Search string
	// IncludeSchema adds each model's input JSON Schema. It makes the
	// response much larger.
	IncludeSchema bool
	// IncludeExamples adds inputs that already validate against that schema,
	// the fastest way to get a first call working.
	IncludeExamples bool
}

ListModelsOptions filters the catalogue. A zero field means "do not filter".

type ListTasksOptions

type ListTasksOptions struct {
	// From and To are UTC dates in YYYY-MM-DD form and bound a half-open
	// interval [From, To) of at most 92 days. Left empty, the server defaults
	// To to tomorrow in UTC and From to seven days before it.
	From string
	To   string
	// State is one of the six task states.
	State string
	// Model is an exact catalogue identifier or a declared alias.
	Model string
	// Limit is the page size, 1 to 100, 20 by default on the server. The upper
	// bound is deliberately not enforced here.
	Limit int
	// Cursor is the NextCursor of the previous page, passed through unchanged.
	Cursor string
}

ListTasksOptions filters task history. A zero field means "do not filter".

type LoRAImport

type LoRAImport struct {
	// URL is a Hugging Face repo id (owner/repo, when the repo has exactly one
	// .safetensors file), a Hugging Face repo or file page, or a direct https
	// link to a .safetensors file.
	URL string `json:"url"`
	// Token is an optional Hugging Face access token or Civitai API key, used
	// only for this download and never stored.
	Token string `json:"token,omitempty"`
}

LoRAImport is the body of ImportLoRA.

type Model

type Model struct {
	// Model is the exact call name to pass as CreateTaskInput.Model, in
	// <publisher>/<model>/<task> form.
	Model  string `json:"model"`
	Family string `json:"family"`
	// DisplayName is for your UI, not for calls.
	DisplayName string `json:"displayName"`
	// Provider is the model's creator, the lab that built it.
	Provider string `json:"provider"`
	// Modality is image, video or audio.
	Modality string `json:"modality"`
	// Tasks holds exactly one classification and is not always the suffix of
	// Model: editing endpoints publish as .../edit and classify as
	// image-to-image (or image-edit for single-purpose tools). Call with
	// Model; group with Tasks. The set is open.
	Tasks []string `json:"tasks"`
	Async bool     `json:"async"`
	// Toolkit groups related endpoints for display (subject-swap,
	// studio-tools). Empty when the model belongs to no toolkit.
	Toolkit string `json:"toolkit,omitempty"`
	// TaskTimeoutSeconds is the platform execution deadline, not the length
	// of the media that comes out.
	TaskTimeoutSeconds int `json:"taskTimeoutSeconds"`
	// MaxOutputDurationSeconds is the longest output the schema allows, 0
	// when the model declares none.
	MaxOutputDurationSeconds int `json:"maxOutputDurationSeconds,omitempty"`
	// Enabled and Available both have to be true before a model can be called.
	Enabled   bool `json:"enabled"`
	Available bool `json:"available"`
	// QuantityField names the input field that multiplies the price.
	QuantityField string        `json:"quantityField"`
	Credits       *ModelCredits `json:"credits,omitempty"`
	// InputSchema is JSON Schema 2020-12 and stays raw on purpose: it is what
	// validates your input and what a form generator renders. Request it with
	// ListModelsOptions.IncludeSchema or read it from GetModel.
	InputSchema json.RawMessage `json:"inputSchema,omitempty"`
	// Version changes whenever the schema or metadata changes; cache on it.
	Version       string         `json:"version"`
	Availability  string         `json:"availability"`
	Badges        []string       `json:"badges"`
	RelatedModels []string       `json:"relatedModels"`
	Examples      []ModelExample `json:"examples,omitempty"`
	UpdatedAt     time.Time      `json:"updatedAt"`
}

Model is one callable endpoint from the catalogue.

type ModelCreditRule

type ModelCreditRule struct {
	When    map[string]any `json:"when"`
	PerUnit float64        `json:"perUnit"`
	Unit    string         `json:"unit,omitempty"`
}

ModelCreditRule is one price tier: it applies when the request matches its When values, where "*" matches the field that sets the tier.

type ModelCredits

type ModelCredits struct {
	// Unit is how the price scales: per_image, per_second, per_request, ...
	// Treat it as open.
	Unit string `json:"unit"`
	// PerUnit is the starting price per unit. It can be fractional.
	PerUnit float64 `json:"perUnit"`
	// Rules lists the price tiers.
	Rules []ModelCreditRule `json:"rules,omitempty"`
}

ModelCredits is a model's price in credits (1 credit = $0.01), the same price popnow.ai shows. Use Quote for the exact amount of one request.

type ModelExample

type ModelExample struct {
	ID         string         `json:"id"`
	Input      map[string]any `json:"input"`
	SortWeight int            `json:"sortWeight"`
}

ModelExample is an input that validates against the model's current schema.

type ModelList

type ModelList struct {
	Total int     `json:"total"`
	Items []Model `json:"items"`
}

ModelList is the ListModels response.

type Options

type Options struct {
	// APIKey defaults to the POPNOW_API_KEY environment variable. Keep it
	// server-side; it is a bearer credential for your credit wallet.
	APIKey string
	// BaseURL defaults to APIBaseURL.
	BaseURL string
	// HTTPClient defaults to a client with no timeout of its own, because
	// every request here already carries a context deadline.
	HTTPClient HTTPDoer
	// UserAgent replaces the default UserAgent. Anyone embedding this client
	// in their own tool can name that tool here; keep it non-empty and
	// specific, because generic default agents are refused at the edge.
	UserAgent string
	// AcceptLanguage, when set, is sent as Accept-Language to choose the
	// language of error messages (for example "de" or "ja"). Codes never
	// change with the language.
	AcceptLanguage string

	RequestTimeout time.Duration
	UploadTimeout  time.Duration
	WaitTimeout    time.Duration
	// MaxRetries is the number of extra attempts a replayable request gets.
	// Set it to -1 to disable retries entirely.
	MaxRetries int

	// Now and Rand exist for deterministic tests.
	Now  func() time.Time
	Rand func() float64
	// Sleep must return the context error if the context ends first.
	Sleep func(ctx context.Context, d time.Duration) error
}

Options configures New. Every zero value falls back to a documented default.

type OutputAsset

type OutputAsset struct {
	// Key identifies this output for CreateDownloadURL and CreateAsset.
	Key string `json:"key,omitempty"`
	// URL is a signed link that works on its own. Never attach your API key to
	// it: it is already a bearer grant. It is absent while Pending or
	// Unavailable is true.
	URL string `json:"url,omitempty"`
	// ExpiresAt is this link's expiry, normally about twenty minutes out. It
	// is not the retention deadline of the result; poll again for a fresh link.
	ExpiresAt       *time.Time `json:"expiresAt,omitempty"`
	MIME            string     `json:"mime,omitempty"`
	Width           int        `json:"width,omitempty"`
	Height          int        `json:"height,omitempty"`
	DurationSeconds float64    `json:"durationSeconds,omitempty"`
	Bytes           int64      `json:"bytes,omitempty"`
	// Role is video, image, audio or a model-specific role such as segment.
	Role string `json:"role,omitempty"`
	// Pending means the artefact is still being copied into storage.
	Pending bool `json:"pending,omitempty"`
	// Unavailable means it will not arrive; do not keep waiting for it.
	Unavailable bool `json:"unavailable,omitempty"`
	// Deleted means the owner permanently deleted this one output. It always
	// comes with Unavailable and no Key or URL.
	Deleted bool `json:"deleted,omitempty"`
	// Layer is set only on the outputs of a layer-decomposition task.
	Layer *AssetLayer `json:"layer,omitempty"`
}

OutputAsset is one generated artefact of a task.

type PurgeResult

type PurgeResult struct {
	TaskID string `json:"taskId"`
	// ContentState is purged after a successful destroy and on every repeat.
	// Still present means nothing was removed and the call may be retried.
	ContentState string `json:"contentState"`
	// PurgedAt is the time of the first destroy, not of this call.
	PurgedAt         *time.Time `json:"purgedAt,omitempty"`
	ContentRemovedBy string     `json:"contentRemovedBy,omitempty"`
	// BillingRetained is always true: destroying content never touches the
	// ledger or the charged amount.
	BillingRetained bool `json:"billingRetained"`
	// MediaDeletionPending is true while the background sweep is still
	// removing the stored objects, which takes about a minute.
	MediaDeletionPending bool `json:"mediaDeletionPending"`
}

PurgeResult reports what PurgeTask destroyed.

type Quote

type Quote struct {
	// QuoteID goes back to CreateTask together with MaxCredits.
	QuoteID string `json:"quoteId"`
	// Model is the resolved endpoint identifier.
	Model string `json:"model"`
	// MaxCredits is the most this request can cost, in credits. It is held
	// when the task is accepted. Pass it back as ExpectedCredits.
	MaxCredits int64 `json:"maxCredits"`
	// ExpectedCredits is what the request usually costs. It equals MaxCredits
	// unless the final charge depends on the output, and the charge never
	// exceeds MaxCredits.
	ExpectedCredits int64 `json:"expectedCredits"`
	// EstimatedCost and MaxCharge are the same amounts as USD decimal strings.
	EstimatedCost string `json:"estimatedCost"`
	MaxCharge     string `json:"maxCharge"`
	Currency      string `json:"currency"`
	// Quantity and Unit explain how the amount was reached.
	Quantity  string    `json:"quantity"`
	Unit      string    `json:"unit"`
	ExpiresAt time.Time `json:"expiresAt"`
}

Quote is a signed price for one exact request. It reserves nothing and guarantees no capacity; it only fixes the price for five minutes.

type RetryTaskResult

type RetryTaskResult struct {
	CreateTaskResult
	SourceTaskID string `json:"sourceTaskId"`
}

RetryTaskResult additionally names the task this one was created from.

type Task

type Task struct {
	TaskID string `json:"taskId"`
	// SourceTaskID is set when this task came from RetryTask.
	SourceTaskID string `json:"sourceTaskId,omitempty"`
	Model        string `json:"model"`
	State        string `json:"state"`
	// Input is the normalized input, including the random seed the service
	// picked when you omitted one. Omitted once retention has redacted it.
	Input  map[string]any `json:"input,omitempty"`
	Output *TaskOutput    `json:"output,omitempty"`
	// ErrorCode is one of the nine ErrorCode constants. Branch on it.
	ErrorCode string `json:"errorCode,omitempty"`
	// ErrorMessage explains the failure. Do not parse it.
	ErrorMessage string `json:"errorMessage,omitempty"`
	// Credits and Cost are the same amount: the final charge once Settled is
	// true, and the held estimate before that. A failed or expired task
	// normally releases its hold in full; read them once Settled is true
	// instead of assuming.
	Credits int64  `json:"credits"`
	Cost    string `json:"cost"`
	Settled bool   `json:"settled"`

	CreatedAt        time.Time      `json:"createdAt"`
	DeadlineAt       *time.Time     `json:"deadlineAt,omitempty"`
	CompletedAt      *time.Time     `json:"completedAt,omitempty"`
	ContentState     string         `json:"contentState,omitempty"`
	ContentRemovedBy string         `json:"contentRemovedBy,omitempty"`
	PurgedAt         *time.Time     `json:"purgedAt,omitempty"`
	Retention        *TaskRetention `json:"retention,omitempty"`
}

Task is the full record returned by GetTask. Terminal and non-terminal states share this shape.

func (*Task) HasPendingAssets

func (t *Task) HasPendingAssets() bool

HasPendingAssets reports whether an artefact is still being copied. A task can be succeeded with its files not yet linkable.

func (*Task) InFlight

func (t *Task) InFlight() bool

InFlight reports whether the task is still queued or running. Everything else is terminal, including a state this client has never heard of.

func (*Task) ReadyAssets

func (t *Task) ReadyAssets() []OutputAsset

ReadyAssets returns the artefacts that have a usable URL right now.

type TaskList

type TaskList struct {
	Items   []TaskListItem `json:"items"`
	HasMore bool           `json:"hasMore"`
	// NextCursor is present only when HasMore is true. It is opaque.
	NextCursor string `json:"nextCursor,omitempty"`
}

TaskList is one page of task history.

type TaskListItem

type TaskListItem struct {
	TaskID string `json:"taskId"`
	// RequestID is the X-Request-Id of the request that created the task.
	RequestID string `json:"requestId,omitempty"`
	Model     string `json:"model"`
	State     string `json:"state"`
	// Credits and Cost are the final charge once Settled is true, and the
	// held estimate before that.
	Credits    int64     `json:"credits"`
	Cost       string    `json:"cost"`
	Settled    bool      `json:"settled"`
	CreatedAt  time.Time `json:"createdAt"`
	DeadlineAt time.Time `json:"deadlineAt"`
	// CompletedAt is nil while the task is still in flight.
	CompletedAt *time.Time `json:"completedAt,omitempty"`
}

TaskListItem is one row of task history: metadata only, with no input, output or signed media URL. Read a selected task with GetTask.

type TaskOutput

type TaskOutput struct {
	// Text is the plain-text result. For a multichannel transcription it holds
	// one line per channel, in channel order.
	Text string `json:"text,omitempty"`
	// Transcript is the structured speech-to-text result. Nil on every other
	// task.
	Transcript *TaskTranscript `json:"transcript,omitempty"`
	Assets     []OutputAsset   `json:"assets,omitempty"`
}

TaskOutput is the result payload of a finished task.

Not every endpoint produces files. Transcription answers in Text and returns no assets, so check what you actually got before reaching for Assets[0].

type TaskRetention

type TaskRetention struct {
	OutputsExpireAt time.Time `json:"outputsExpireAt"`
	PromptsExpireAt time.Time `json:"promptsExpireAt"`
	Source          string    `json:"source"`
}

TaskRetention says when this task's content disappears and which layer decided it: header, account or platform.

type TaskTranscript

type TaskTranscript struct {
	Text string `json:"text,omitempty"`
	// Language is the detected language as the model reports it ("en");
	// LanguageCode is the ISO 639-3 form ("eng") when known.
	Language     string `json:"language,omitempty"`
	LanguageCode string `json:"languageCode,omitempty"`
	// LanguageProbability is a pointer because "not reported" and "zero
	// confidence" are different.
	LanguageProbability *float64                `json:"languageProbability,omitempty"`
	DurationSeconds     float64                 `json:"durationSeconds,omitempty"`
	Words               []TaskTranscriptWord    `json:"words,omitempty"`
	Channels            []TaskTranscriptChannel `json:"channels,omitempty"`
}

TaskTranscript is a structured transcription.

type TaskTranscriptChannel

type TaskTranscriptChannel struct {
	Index        int                  `json:"index"`
	Text         string               `json:"text,omitempty"`
	Language     string               `json:"language,omitempty"`
	LanguageCode string               `json:"languageCode,omitempty"`
	Words        []TaskTranscriptWord `json:"words,omitempty"`
}

TaskTranscriptChannel is the transcript of one audio channel.

type TaskTranscriptWord

type TaskTranscriptWord struct {
	Text      string  `json:"text"`
	Start     float64 `json:"start"`
	End       float64 `json:"end"`
	Type      string  `json:"type,omitempty"`
	SpeakerID string  `json:"speakerId,omitempty"`
}

TaskTranscriptWord is one timed segment of a transcript, in seconds.

type UpdateAssetInput

type UpdateAssetInput struct {
	Name     *string `json:"name,omitempty"`
	Favorite *bool   `json:"favorite,omitempty"`
}

UpdateAssetInput renames an asset or marks it as a favorite. Only what is set changes; both are pointers so that "leave alone" and "clear" differ.

type UploadTicket

type UploadTicket struct {
	FileID string `json:"fileId"`
	// Key is a compatibility alias of the final URI and is unusable until the
	// commit succeeds. Put UploadedFile.URI in task input, never this.
	Key       string `json:"key"`
	UploadURL string `json:"uploadUrl"`
	Method    string `json:"method"`
	// Headers must be forwarded to the PUT exactly as given.
	Headers map[string]string `json:"headers"`
	// ExpiresAt covers the PUT and the commit together: about twenty minutes,
	// two hours for LoRA weights. Ask for a ticket right before uploading.
	ExpiresAt time.Time `json:"expiresAt"`
	// MaxBytes is the authoritative ceiling for this upload.
	MaxBytes int64 `json:"maxBytes"`
}

UploadTicket is a presigned slot for exactly one file of a declared size.

type UploadedFile

type UploadedFile struct {
	FileID string `json:"fileId"`
	Status string `json:"status"`
	Bytes  int64  `json:"bytes"`
	// ContentType is what the server verified.
	ContentType string `json:"contentType"`
	SHA256      string `json:"sha256"`
	// URI is the popnow://f/fil_... value to put in task input (or in
	// loras[].path for LoRA weights). Use this one, not the upload URL, not a
	// storage key, not the bare file ID.
	URI       string    `json:"uri"`
	ExpiresAt time.Time `json:"expiresAt"`
	// DurationSeconds is a decimal string, present for audio and video.
	DurationSeconds string `json:"durationSeconds,omitempty"`
	Width           int    `json:"width,omitempty"`
	Height          int    `json:"height,omitempty"`
}

UploadedFile is a committed file, ready to be referenced by a task.

type UsageDay

type UsageDay struct {
	Day       string `json:"day"`
	Calls     int64  `json:"calls"`
	Succeeded int64  `json:"succeeded"`
	Failed    int64  `json:"failed"`
	Credits   int64  `json:"credits"`
	Spend     string `json:"spend"`
}

UsageDay is one day of activity. Day is a UTC calendar date, YYYY-MM-DD.

type UsageModel

type UsageModel struct {
	Model     string `json:"model"`
	Calls     int64  `json:"calls"`
	Succeeded int64  `json:"succeeded"`
	Failed    int64  `json:"failed"`
	Credits   int64  `json:"credits"`
	Spend     string `json:"spend"`
}

UsageModel is one model's share of the same window.

type UsageOptions

type UsageOptions struct {
	From string
	To   string
}

UsageOptions bounds a usage query to a half-open UTC interval [From, To) of at most 92 days, both as YYYY-MM-DD. Left empty the server uses the last seven days.

type UsageReport

type UsageReport struct {
	// From and To echo the window actually used. Read them rather than
	// assuming your own: left empty, the server picks.
	From     string `json:"from"`
	To       string `json:"to"`
	Currency string `json:"currency"`
	// TotalCredits and TotalSpend count settled charges only, never holds.
	TotalCalls   int64  `json:"totalCalls"`
	TotalCredits int64  `json:"totalCredits"`
	TotalSpend   string `json:"totalSpend"`
	// Days and Models are sparse: a day or a model with no activity is absent.
	Days   []UsageDay   `json:"days"`
	Models []UsageModel `json:"models"`
}

UsageReport is settled spend and call counts over one window.

type WaitOptions

type WaitOptions struct {
	// Timeout is the local bound on the whole wait, DefaultWaitTimeout by
	// default. It is not a service level.
	Timeout time.Duration
	// Interval is the first gap between polls, DefaultPollInterval by default.
	Interval time.Duration
	// MaxInterval caps how far that gap grows, MaxPollInterval by default.
	MaxInterval time.Duration
	// AcceptPendingAssets returns a terminal task even when its artefacts are
	// still being copied, so they have no URL yet. Leave it false to get back
	// a record you can actually download from.
	AcceptPendingAssets bool
}

WaitOptions tunes WaitForTerminal. Every zero field takes its default.

type WaitTimeoutError

type WaitTimeoutError struct {
	TaskID    string
	Timeout   time.Duration
	LastState string
}

WaitTimeoutError means a local polling deadline elapsed. It is not evidence that the task failed: the generation is still running somewhere, so store TaskID and reconcile it later instead of submitting the work twice.

func (*WaitTimeoutError) Error

func (e *WaitTimeoutError) Error() string

func (*WaitTimeoutError) Unwrap

func (e *WaitTimeoutError) Unwrap() error

Unwrap lets errors.Is(err, context.DeadlineExceeded) recognise this.

type WebhookDelivery

type WebhookDelivery struct {
	// TaskID is the task the signature was computed over. Use this one, not a
	// value you read out of the body yourself.
	TaskID string
	// DeliveryID is the envelope's request_id. It stays the same across
	// retries: store it with a unique index and ignore repeats.
	DeliveryID string
	// PayloadVersion is the X-Webhook-Payload-Version header, "2" for every
	// current integration.
	PayloadVersion string
	// Task is the task record carried by a version 2 delivery, the same shape
	// GetTask returns. Nil for version 1. Its signed links expire after about
	// twenty minutes; store the task ID, not the link.
	Task *Task
}

WebhookDelivery is a verified task callback.

func VerifyWebhook

func VerifyWebhook(secret string, header http.Header, rawBody []byte, tolerance time.Duration) (*WebhookDelivery, error)

VerifyWebhook authenticates one callBackUrl delivery (a task callback).

The signature is

base64( HMAC-SHA256( secret, "<taskId>.<timestamp>.<sha256_hex(rawBody)>" ) )

where secret is the account's webhook signing secret (POPNOW_WEBHOOK_SECRET in the API console), timestamp is X-Webhook-Timestamp, and taskId comes from data.taskId in payload version 2 and from top-level task_id in the older version 1, as X-Webhook-Payload-Version says.

rawBody must be the exact bytes that arrived. Decoding and re-encoding the JSON first changes key order, spacing and escaping, and the digest then fails every time for a reason that looks like a wrong secret.

tolerance bounds the accepted clock skew; pass 0 for WebhookTolerance. Every retry is signed again with a fresh timestamp.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL