Curaword
Developers
Curaword for developers · API version 1 · October 2026

Integration & API Guide

How your software sends documents to Curaword for certified translation, pays for them, approves the work and collects the certified PDF, without anyone visiting our site. The API Reference, in the side panel, lists every endpoint, field and error, with a Swagger view and the OpenAPI file.

Base addresshttps://curaword.com
AuthenticationAuthorization: Bearer cw_live_…
FormatsJSON; uploads as multipart/form-data
Moneysmallest unit: 5000 = $50.00

1Who this is for

Curaword produces certified translations of official documents: passports, civil registry records, diplomas, court papers. A sworn translator reviews every translation and signs a Certificate of Translation that anyone can verify. This API lets your software order those translations directly.

Two kinds of account use it. Both call the same endpoints; a few fields differ.

Integrator

A legal-tech or case-management product that sends documents on behalf of the offices using it. Every order names the office it is for. You earn a per-page share on every order that is completed, paid out to your bank account.

Office with its own system

A law or immigration office that is already a Curaword partner and connects its own portal or back-office tool. Orders are placed on one of the office's promotion codes and earn its partner commission, exactly as a customer's order with that code would.

The end client, the person the translation is for, has no Curaword account and hears nothing from us. Only the name you send travels with the order. You are our counterpart: we write to your account, and you decide what your client sees.

2Getting started

You needWho does itWhere
An account with the API onCurawordAn integrator account is created by us with the API already on. For an office, we turn the API on for its existing partner account. Until then the page says it is off and no key can be made.
An API keyYouYour Curaword page: Keys (integrators) or Your own system (offices). The key is shown once.
A webhook address (optional)YouYour Curaword page, under Being told. Or read the event feed instead.
A payout account (integrators)YouYour Curaword page, under Where your money goes. See your account page.

Your API key

Send it in the Authorization header of every request:

Authorization: Bearer cw_live_3q2W…
  • A key starts with cw_live_ and is 51 characters long. We store only a hash of it, so we cannot show it to you again. If you lose it, make a new one.
  • Not in a query string, not as a cookie: neither is accepted. A request without a key that looks like ours is refused before its upload is read.
  • It is a server-side secret. Keep it in your server's configuration, never in a browser, a mobile app or a repository.
  • You may hold up to 10 working keys. One per system or environment is a good pattern. All your keys see all your account's orders.
  • To stop a key, revoke it on your page. It stops working on its next request, and revoking cannot be undone.
  • An office may tie a key to one of its promotion codes, so that orders sent with that key go on that code. An integrator's keys carry no code.
AnswerMeaning
401No key, a mistyped or revoked key, or your account is switched off. The body is {"detail": "a working API key is required"}.
403The key is real but the API is not turned on for your account: "the API is not turned on for this account". Ask us.

3The whole flow

One document, from upload to certified PDF, in six calls. $KEY is your API key; $ID is the order id from step 1.

  1. Send the document and get its price.

    curl https://curaword.com/v1/api/orders \
      -H "Authorization: Bearer $KEY" \
      -H "Idempotency-Key: $(uuidgen)" \
      -F file=@passport.pdf \
      -F source_language=tr \
      -F target_language=en \
      -F client_name="Ayşe Yılmaz" \
      -F external_reference="case-4711/passport" \
      -F office_reference="office-17" \
      -F office_name="Yılmaz Law Office"

    The last two fields are required from an integrator; an office's own system leaves them out. The answer:

    { "id": "614a3ca6-8505-4fbc-ae79-c6cc72a9ace4", "status": "awaiting_payment",
      "page_count": 2, "unit_price_minor": 2500, "total_minor": 5000, "currency": "USD",
      "quote_expires_at": "2026-10-25T20:12:41Z", … }
  2. Get a payment link and open it, or pass it to whoever pays.

    curl -X POST https://curaword.com/v1/api/orders/$ID/checkout \
      -H "Authorization: Bearer $KEY" \
      -H "Content-Type: application/json" \
      -d '{"success_url":"https://app.example/cases/4711?paid=1","cancel_url":"https://app.example/cases/4711"}'
    { "checkout_url": "https://checkout.stripe.com/c/pay/cs_…", "expires_at": "2026-10-19T20:14:02Z" }
  3. Wait for the translation. A webhook tells you when the order becomes awaiting_approval, or read the order now and then:

    curl https://curaword.com/v1/api/orders/$ID -H "Authorization: Bearer $KEY"
  4. Look at the watermarked preview.

    curl -o preview.pdf https://curaword.com/v1/api/orders/$ID/preview -H "Authorization: Bearer $KEY"
  5. Approve it, or reject it with a reason.

    curl -X POST https://curaword.com/v1/api/orders/$ID/approve -H "Authorization: Bearer $KEY"
  6. Collect the certified PDF and its certificate record.

    curl -o certified.pdf https://curaword.com/v1/api/orders/$ID/deliverable -H "Authorization: Bearer $KEY"
    curl https://curaword.com/v1/api/orders/$ID/certificate -H "Authorization: Bearer $KEY"

4How an order lives

An order is always in one of eight statuses. These eight words are the whole contract: we have finer states inside, and keep them to ourselves so we can change them without breaking you. Treat a status you do not know as "wait".

statusWhat it meansWhat you can do
awaiting_paymentPriced. Nothing has started.Get a payment link, or cancel.
processingPaid. We are preparing the translation draft.Wait.
in_reviewThe draft is ready; a sworn translator takes it, checks it against the original and corrects it.Wait.
awaiting_approvalFinished and waiting for your decision.Fetch the preview; approve or reject.
in_revisionYou rejected it. The translator is answering your reason, or Curaword is deciding a disagreement.Wait. Preview still available.
completedApproved. The certified document is yours.Download the deliverable and the certificate.
refundedWe could not complete it, or a dispute was decided in your favour. The payment went back.Nothing.
cancelledWithdrawn before payment, by you or because the quote lapsed.Nothing.

Turns worth knowing

  • Nothing is approved for you by a timer. An order waits in awaiting_approval until you decide. Its 30-day storage clock does not start until it ends, so leaving orders undecided keeps them open.
  • completed can arrive without your approval: when Curaword decides a disputed rejection in favour of the work, or when you ask us to approve on your behalf.
  • An order can go back. If a page has to be prepared again, an order in in_review returns to processing and then to in_review. You may see order.processing twice.
  • An order may end refunded while processing or in_review, or after a rejection, but never once it is completed.

Two clocks

  • A quote stands for 7 days. quote_expires_at says when. Some time within the following hour, an order still awaiting_payment is withdrawn (cancelled), its document is deleted, and your account gets an e-mail. If a payment link for it is still open, the order waits until that link has expired, so a link asked for on the last day can still be paid.
  • Files are kept for 30 days after an order ends (completed or refunded; a cancelled order's document is deleted when it is cancelled). documents_expire_at says until when; documents_purged_at is set when they are gone. After that the preview and deliverable answer 409; the certificate record and its public verification keep working. Collect what you need and keep your own copy.

Money is always in the currency's smallest unit: 5000 is $50.00.

5Sending a document

POST /v1/api/orders as multipart/form-data. One order is one document. Five documents for one applicant are five orders; give them the same case_reference and pay for them with one link.

FieldRequiredRules
fileyesThe document, up to 100 MiB (104,857,600 bytes). PDF, Word .docx, or an image: .png .jpg .jpeg .tif .tiff .bmp .webp. We tell the type by the file name's extension, so send the real name. An image is one page; a scan of several pages belongs in one PDF.
source_languageyesLanguage of the document, e.g. tr. 2–16 characters.
target_languageyesLanguage to translate into, e.g. en. The pair must be one we translate: GET /v1/languages, no key needed.
client_nameyesWho the translation is for. 1–120 characters.
external_referenceyesYour own id for this document, 1–128 characters. Returned as sent; need not be unique. On a shared payment link it is shown to the payer (its first 80 characters), so put nothing in it they should not read.
case_referencenoYour own id for the case the document belongs to, up to 128 characters. For grouping, filtering and paying several orders at once.
office_referenceintegrators: yesYour own id for the office this document is for, up to 128 characters. It is what makes two orders the same office's.
office_nameintegrators: yesThe office's name, up to 200 characters. Required on every order; the name sent with the office's first order is the one kept.
office_emailnoIntegrators: the office's e-mail address, if you have it. It helps us recognise an office that already works with us. Kept from the office's first order, where anything that is not an address is refused.
promotion_codenoOffices only: one of your own open codes. Without it, the key's code is used, or else your oldest open code.
metadatanoA JSON object of strings, sent as text: up to 20 keys, keys up to 40 characters, values up to 500. Returned as sent.
name_spellingsnoA JSON list, sent as text. See below.
HeaderRequiredRules
Idempotency-KeyyesAny string you make up, 1–255 characters, unique per order you intend to create. A UUID is fine.

An integrator that sends promotion_code, or an office that sends any office_* field, is refused with 422.

The answer

201 Created with the order:

{
  "id": "614a3ca6-8505-4fbc-ae79-c6cc72a9ace4",
  "status": "awaiting_payment",
  "external_reference": "case-4711/passport",
  "case_reference": "case-4711",
  "client_name": "Ayşe Yılmaz",
  "source_filename": "passport.pdf",
  "source_language": "tr",
  "target_language": "en",
  "page_count": 2,
  "unit_price_minor": 2500,
  "total_minor": 5000,
  "currency": "USD",
  "promotion_code": null,
  "metadata": {"branch": "Ankara"},
  "office_reference": "office-17",
  "office_name": "Yılmaz Law Office",
  "name_spellings": [],
  "created_at": "2026-10-18T20:12:41.118000Z",
  "updated_at": "2026-10-18T20:12:41.118000Z",
  "quote_expires_at": "2026-10-25T20:12:41.118000Z",
  "documents_expire_at": null,
  "documents_purged_at": null
}
  • The price is per page and we count the pages. You learn the total here, before anything is charged, and decide whether to pay. A PDF counts its pages, a Word document is laid out and counted, an image is one page.
  • promotion_code is null on an integrator's order; office_reference and office_name are null on an office's own. Read every field that can be null as "may be absent".
  • An integrator's order shows the office's reference and name as they were first sent.

Sending it twice

Uploads time out, and from your side a timeout and a failure look alike. Send the request again with the same Idempotency-Key:

What you sendAnswer
The same key and the same request200 OK with the order the first request made, as it is now, and the header Idempotent-Replayed: true. No second order, no second charge.
The same key, a different document or different fields422 "this Idempotency-Key was used for a different request".
A new keyA new order, even for an identical document.
  • "The same request" means the same file bytes and the same fields. The file name does not count.
  • Keys belong to your account, not to one API key: two of your servers using different API keys share one space of idempotency keys.
  • A key is remembered for 24 hours. A request that failed (4xx) made nothing, so its key is free to reuse with the corrected request.

How names are spelled

A name has no translation, only a spelling, and the right one is on a document we may never see: the passport. If you know how a name must be written, send it:

-F 'name_spellings=[{"as_written":"Ayşe Yılmaz","spell_as":"AYSE YILMAZ"},
                    {"as_written":"Şanlıurfa","spell_as":"Sanliurfa"}]'

as_written is the name as it appears in the document; spell_as is how it must appear in the translation. Up to 20 pairs, 1–120 characters a side, no other keys. The sworn translator who reviews the work is shown these and follows them. They are not given to the first draft, so if a name still arrives spelled otherwise in the preview, reject with the correct spelling.

Offices you send for (integrators)

The first order that names a new office_reference records that office with us. No account is opened for it and nobody is e-mailed. Whether the office also earns from orders you send for it is arranged between the office and Curaword; it never changes your own share.

When it is refused

AnswerWhy
413The file is over 100 MiB. A request far larger may be stopped by the server in front of ours without a JSON body; the status is the answer.
422A field is missing or wrong (detail names it); the language pair is not one we translate; the file cannot be read or has no pages; an office field or code where it does not belong; or the idempotency key was used for something else.
429Too many unpaid quotes are open: 2,000 for an integrator, 200 for an office's system. Pay for or cancel some first.

6Payment

Every order is paid through a payment link on our payment provider's page (Stripe). Whoever opens the link pays: your own staff, the office, or the person the translation is for if you forward it. We do not ask who. A refund goes back to whoever paid, the same way.

One order

POST /v1/api/orders/{id}/checkout, with an optional JSON body:

{ "success_url": "https://app.example/cases/4711?paid=1",
  "cancel_url":  "https://app.example/cases/4711" }
{ "checkout_url": "https://checkout.stripe.com/c/pay/cs_…",
  "expires_at":   "2026-10-19T20:14:02.411000Z" }
  • After paying, the payer is sent to success_url; if they leave, to cancel_url. Both must be https addresses with a host, no credentials and no spaces, up to 2,000 characters. Without them, the payer lands on a Curaword page that needs no sign-in.
  • Arriving at success_url does not mean the order is paid. Anyone can open that address. Wait for the status processing or the order.processing event.
  • Asking again returns the same link while it lives, with the first request's return addresses. In its last five minutes you get a fresh one instead.
  • A link lives about 24 hours; expires_at is the provider's own deadline.

One link for several orders

An applicant has five documents. Instead of five links, ask for one: POST /v1/api/checkouts with either a case or a list of orders.

{ "case_reference": "case-4711" }
{ "order_ids": ["614a3ca6-…", "42a29ed3-…", "9d1e7b02-…"] }

success_url and cancel_url may be given as above. The answer lists what is on the link:

{ "checkout_url": "https://checkout.stripe.com/c/pay/cs_…",
  "expires_at": "2026-10-19T20:14:02.411000Z",
  "total_minor": 15000, "currency": "USD",
  "orders": [
    { "id": "614a3ca6-…", "external_reference": "case-4711/passport", "total_minor": 5000 },
    { "id": "42a29ed3-…", "external_reference": "case-4711/diploma",  "total_minor": 7500 },
    { "id": "9d1e7b02-…", "external_reference": "case-4711/birth",    "total_minor": 2500 } ] }
  • Two to twenty orders, all awaiting_payment.
  • A case means its unpaid orders that have no live link of their own. An order you already sent its own link for keeps that link, because the link may be in somebody's hands. To fold such an order in, name it in order_ids; its own link is then closed.
  • The payment page shows one line per order: Translation — 2 page(s) · case-4711/passport (the first 80 characters of your external_reference). A single order's own link shows only Translation — 2 page(s). The client name is never shown.
  • Each order is still its own order afterwards: its own status, events, approval, and refund if it comes to that.
  • Asking again for the same set returns the same link. Asking for a different set closes the links those orders were on and makes a new one; an order that was on an old link and is not in the new set is left with none.
  • An order on a shared link cannot get a link of its own until the shared one expires or is replaced (409).

Cancelling

POST /v1/api/orders/{id}/cancel withdraws an order that is still awaiting_payment and deletes its document. A payment link the order is on is closed first, for every order on it; the others can be given a new one.

When payment goes wrong

AnswerWhyWhat to do
409The order is not awaiting payment; it is on a shared link; a link it was on has just been paid; or the total is below the provider's minimum charge.Read the order(s) again. The minimum-charge case will not change on retry.
422A return address is not acceptable; or the group request has fewer than two or more than twenty orders, or both or neither of the two selectors.Fix the request.
502The payment provider could not be reached, or a link could not be closed.Try again shortly.

If a payment arrives for an order that was withdrawn in the meantime, we refund it by hand; it does not become an order again.

7Following your orders

Three ways to know what happened, and most integrations use two of them:

  • Webhooks: we call your server when an order's status changes. Fastest.
  • The event feed: you ask us for everything since your last read. Simplest to run, and the way to catch up after downtime.
  • Reading orders: GET /v1/api/orders/{id} for one, GET /v1/api/orders for a list.

Listing

GET /v1/api/orders returns your orders, newest first.

Query
statusOne of the eight statuses.
external_referenceExactly as you sent it.
case_referenceExactly as you sent it.
limit1 to 200; default 50.
afterThe next of the page before.
{ "items": [ … ], "next": "MjAyNi0xMC0xOFQyMDoxMjo0MS…" }

next is null on the last page. An order that is not your account's answers 404, exactly like one that does not exist.

8Approving or rejecting

When an order is awaiting_approval, look at it and decide. Your decision is final for that delivery.

Preview

GET /v1/api/orders/{id}/preview returns the translation as a watermarked PDF, for looking at, not for using. It is available while the order is awaiting_approval, in_revision or completed, until the files are deleted. Every fetch is recorded against the key that made it.

Approve

POST /v1/api/orders/{id}/approve. The order becomes completed and the certified document is released. It cannot be taken back.

Reject

POST /v1/api/orders/{id}/reject
{ "reason": "The surname is spelled Yilmaz in the passport, not Yılmaz." }

A reason is required (1–2,000 characters), and it reaches the translator as you wrote it: say what is wrong and where. The order becomes in_revision. Then one of two things happens:

  • The translator agrees and corrects the work: in_review, then awaiting_approval again with a new preview, for a new decision.
  • The translator disagrees, and Curaword decides: the work stands (completed), it is corrected (in_review then awaiting_approval), or you are refunded (refunded).

What became of a rejection

GET /v1/api/orders/{id}/rejections lists every rejection of the order and its outcome, oldest first, paged like the order list:

{ "items": [
    { "id": "5d0b1f7a-…", "order_id": "614a3ca6-…",
      "reason": "The surname is spelled Yilmaz in the passport, not Yılmaz.",
      "created_at": "2026-10-18T21:52:03Z",
      "accepted_at": "2026-10-18T22:10:44Z",
      "contested_at": null, "contest_note": null,
      "resolved_at": null, "resolution": null, "resolution_note": null } ],
  "next": null }

resolution is one of approved_work (the work stands), returned_for_correction, or refunded_customer.

9The certified translation

When an order is completed:

GET /v1/api/orders/{id}/deliverable returns the certified translation as a PDF download. Its last page is the Certificate of Translation, signed by the sworn translator.

GET /v1/api/orders/{id}/certificate returns the record of what was certified:

{ "version": 1,
  "verification_code": "8F3A1C7D9E24",
  "sha256": "8192da1ce47a…",
  "page_count": 2,
  "certificate_name": "Mehmet Demir, sworn translator (TR–EN)",
  "issued_on": "2026-10-18T21:40:11Z" }
  • sha256 is the digest of the PDF. Compute it over the file you downloaded; if it differs, the file is not the document that was certified.
  • page_count counts the translated pages, not the certificate page.
  • verification_code is printed on the certificate. A translation delivered again after a rejection gets a new version and a new code.
  • Before completed, both answer 409. After the files are deleted, the deliverable answers 409 but the certificate record still answers.

Anyone can verify a certificate

A consulate clerk or a registrar holding the paper can check it, with no account: GET /v1/verify/{code} (the code is not case-sensitive). The answer says what was certified, never the document itself:

{ "order_reference": "614a3ca6-…", "source_language": "tr", "target_language": "en",
  "page_count": 2, "certificate_name": "Mehmet Demir, sworn translator (TR–EN)",
  "issued_on": "2026-10-18T21:40:11Z", "sha256": "8192da1ce47a…" }

10Webhooks

Add up to three addresses on your Curaword page under Being told. Each gets a signing secret (cwwh_…), shown once. When one of your orders enters a new status, we POST to every address. An address receives only events that happen after it was added. Send a test on the page sends a ping.

The address

  • https, on the public internet, on port 443 or 8443, up to 2,000 characters. No credentials and no #fragment in it.
  • We resolve the name at every delivery and call only public addresses. We do not follow redirects.
  • A host name with letters outside ASCII is written in its punycode form: xn--bcher-kva.example, not bücher.example.

What we send

POST /hooks/curaword HTTP/1.1
Content-Type: application/json
User-Agent: Curaword-Webhooks/1
Curaword-Event: order.awaiting_approval
Curaword-Delivery: 0b9c6c55-2d2e-4a4f-8f43-7d1a3a0f2c10
Curaword-Signature: t=1792358411,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

{"created_at":"2026-10-18T21:40:11.904211+00:00","data":{"order":{…}},"id":"c0a80c1e-51d3-4a39-9a6c-0e7f2f6f1b77","type":"order.awaiting_approval"}

data.order is the order exactly as GET /v1/api/orders/{id} returned it at that moment. No document is ever sent.

typeWhen
order.awaiting_paymentYou sent a document and it was priced.
order.processingIt was paid for. (Again if a page is prepared anew.)
order.in_reviewThe draft is ready for the translator.
order.awaiting_approvalIt is ready for your decision.
order.in_revisionYou rejected it.
order.completedIt was approved, by you or by Curaword's decision.
order.refundedIt was refunded.
order.cancelledIt was withdrawn, by you or because the quote lapsed.
pingYou pressed Send a test. data is empty. Only to that address, one at a time.

Webhooks usually arrive within seconds. While our engine is preparing a large document they can wait a few minutes; nothing is lost.

What we expect back

Any 2xx within 10 seconds. Answer first and do your work afterwards. Anything else (another status, a redirect, no answer) and we try again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours. After the seventh attempt we give up on that delivery; the event is still in the feed. Your page shows each delivery, its attempts and your server's last answer.

Verify the signature

Anybody can POST to your address. The signature is how you know it was us.

  1. Take t and v1 from the Curaword-Signature header.
  2. Compute HMAC-SHA256 of t + "." + the raw body, byte for byte, keyed with the address's signing secret, as lowercase hex.
  3. Compare with v1 in constant time.
  4. Refuse it if t is more than five minutes from your clock. Each attempt is signed afresh, so a retry carries a fresh t.

Use the bytes as they arrived. Parse and re-serialise the JSON and the signature will not match. In Flask that is request.get_data(); in FastAPI await request.body(); in Express express.raw({ type: "application/json" }) on that route; in PHP file_get_contents('php://input').

import hashlib, hmac, time

def is_ours(secret: str, header: str, raw_body: bytes) -> bool:
    try:
        fields = dict(part.split("=", 1) for part in (header or "").split(","))
        stamp, given = fields["t"], fields["v1"]
        if not given.isascii() or abs(time.time() - int(stamp)) > 300:
            return False
    except (KeyError, ValueError):
        return False
    expected = hmac.new(secret.encode(), stamp.encode() + b"." + raw_body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(given, expected)

Three things to build for

  • The same event may arrive twice. If we did not hear your answer, we send it again. Keep the id of each event you handled and ignore one you have seen.
  • Events may arrive out of order. A retry of an old event can land after a newer one. data.order.status is what was true then; to act on what is true now, read the order.
  • Do not act on an event alone when it matters. Before releasing a document to your client on the strength of order.completed, fetch the order with your key. The signature says the message is ours; the order says it is still so.

11Event feed and figures

GET /v1/api/events returns everything that happened to your orders, oldest first, in the same shape a webhook delivers, whether or not you have an address. Test pings are not in it.

{ "items": [
    { "id": "c0a80c1e-…", "type": "order.awaiting_approval",
      "created_at": "2026-10-18T21:40:11.904211Z",
      "payload": { "id": "c0a80c1e-…", "type": "order.awaiting_approval", "data": {…}, "created_at": "…" } } ],
  "cursor": "MjAyNi0xMC0xOFQyMTo0MDoxMS…",
  "more": false }
  • Keep cursor and send it as after next time. While more is true, ask again at once with the new cursor; when it is false you are up to date and the next read can wait.
  • Unlike every other list, cursor is given even when there is nothing more, because you will be back. It is null only before your first event. Without after you start from the beginning.
  • limit is 1 to 200, default 50. A cursor that is not ours answers 400.
  • An event appears in the feed 30 seconds after it happens. Events are kept for 30 days; an older cursor still works and resumes at the oldest event we have. If your system may be away longer, list your orders when it is back.

Use the feed instead of webhooks if you cannot receive requests (read it once a minute), or beside them, to find what you missed while your server was down.

A period's figures

GET /v1/api/report?since=2026-10-01T00:00:00Z&until=2026-11-01T00:00:00Z

{ "since": "2026-10-01T00:00:00Z", "until": "2026-11-01T00:00:00Z",
  "orders": 42, "pages": 97, "total_minor": 242500,
  "earned_minor": 19400, "currency": "USD" }
  • Counts the orders your system created in the period that were paid and not refunded: unpaid quotes, cancelled and refunded orders are left out.
  • earned_minor is your share on those orders, read from our ledger, so only orders already completed count.
  • since is inclusive, until exclusive, at most 366 days apart. A time without a zone is read as UTC.

12Your account page and money

Signed in at Curaword, an integrator's page has these parts. An office finds the same tools on its partner page under Your own system.

PartWhat it does
Orders your system sentEvery order, its status and price, as your system sees it.
KeysMake, name and revoke API keys. A new key is shown once.
Being toldWebhook addresses, their signing secrets, Send a test, and the log of every delivery: event, order, your reference, attempts, your server's last status or error, next attempt.
Where your money goesConnect the bank account your earnings are paid to, through our payment provider (Stripe). You leave our page to give your details to Stripe and come back.
StatementEvery amount you have earned, order by order, and whether it has been paid out.

How you earn (integrators)

  • You earn the per-page share in your agreement on every order you send that is completed. The rate is fixed on each order when it is priced, so a later change of rate does not touch orders already sent.
  • The amount is written to your statement at the moment the order is completed. Cancelled and refunded orders earn nothing.
  • We pay your earnings to the bank account you connected. Until it is connected and confirmed by Stripe, earnings wait on your statement.
  • Whether the office you sent for also earns is arranged between that office and Curaword, and never changes your share.

E-mail we send you

Your account receives an e-mail when an order is ready for your approval, when it is completed, when it is refunded after a failure or a dispute, and when an unpaid quote is withdrawn. Webhooks and the feed carry every change; e-mail is for people. The end client receives nothing from us.

13Errors and retries

Every error is JSON with a detail:

{ "detail": "only an order that is awaiting approval can be approved" }

For a field that failed validation, detail is a list naming the field. Every response carries an X-Request-Id header; log it, and send it to us with any 5xx. Write your code against the status code; the detail sentence is for people and may be reworded.

StatusMeaningTry again?
400A cursor that is not one of ours.No.
401No working key.No.
403The API is not on for your account.No; ask us.
404No such order, or not one your account sent.No.
409The order is not in a status that allows this, or the provider refused a total under its minimum.After the status changes; read the order.
413The file is over 100 MiB.No.
422Something in the request is wrong; detail says what. A malformed order id is also 422.After fixing it.
429Too many unpaid quotes open.After paying for or cancelling some.
502The payment provider could not be reached.Yes, shortly.
5xxOur fault.Yes. A create with the same Idempotency-Key; anything else after reading the order.

A good retry policy: retry 502, 503, 504 and network failures with growing pauses (1 s, 2 s, 4 s … up to a minute); never retry 4xx unchanged.

14Limits

LimitValue
File size100 MiB (104,857,600 bytes)
Unpaid quotes open at once2,000 integrator · 200 office
Working API keys10
Webhook addresses3
source_language · target_language2–16 characters
client_name · external_reference · case_reference120 · 128 · 128 characters
office_reference · office_name · office_email128 · 200 · 320 characters
metadata20 keys · 40 a key · 500 a value
name_spellings20 pairs · 120 a side
Rejection reason2,000 characters
Idempotency-Key1–255 characters · remembered 24 hours
Orders on one payment link2–20
Return addresses · webhook address2,000 characters
List page1–200 (default 50)
A quote stands for7 days
A payment link lives forabout 24 hours
Files kept after an order ends30 days
Events and the delivery log kept30 days
Event feed delay30 seconds
Webhook answer time · attempts10 seconds · 7
Report period366 days

15Security and data

  • Keys and secrets stay on your servers. We never ask for them; never send them to us. If one leaks, revoke it on your page and make a new one.
  • Documents travel only over HTTPS, are kept 30 days after the order ends, then deleted. Collect and keep your own copy.
  • Webhooks and e-mails carry no document content, only the order's record.
  • What the payer sees: page counts and amounts, and on a shared link each order's external_reference (first 80 characters). Never the client name.
  • Isolation: you see only your account's orders. Anything else answers 404.
  • Verification answers only what was certified (languages, pages, translator, date, digest), never the document or the client.

16Testing

Before production opens to you, we give you an account on a test copy of Curaword that runs on our payment provider's test mode: no real money moves and no card is charged. Ask your Curaword contact for its address.

  • Pay with the provider's published test cards, for example 4242 4242 4242 4242 with any future date and any CVC.
  • Its accounts and keys belong to it alone. Its keys look exactly like production keys (cw_live_…), so keep them apart by where they are configured.
  • A certificate it issues certifies nothing.
  • Send sample documents, not a client's passport.

17Before you go live

  • The whole flow has run on the test copy: send, pay, preview, approve, collect, and one rejection.
  • Every create request carries an Idempotency-Key, and a retry reuses it.
  • Your production key is in your server's configuration only, and you know how to revoke and replace it.
  • If you use webhooks: the signature is checked on the raw body, the timestamp is checked, duplicates are ignored by event id, and you answer within 10 seconds before doing the work.
  • If you do not: you read GET /v1/api/events with the saved cursor.
  • You read the order before acting on an event that matters.
  • Your code treats an unknown status as "wait" and handles completed arriving without your own approval.
  • You collect deliverables within 30 days, check their sha256, and keep your own copy.
  • You handle 409, 422, 429 and 502 as described, and log the X-Request-Id of any 5xx.
  • Integrators: your payout account is connected on your page.
  • We have turned the API on for your production account.

18Questions

Can one order hold several documents?
No. One order is one document. Group a client's documents with case_reference and pay for them with one link.
Can we pay by card on file, or by invoice?
Not through the API. Every order is paid through a payment link, by whoever opens it.
Who gets e-mails?
Your Curaword account, about the orders your system sent. The end client receives nothing from us.
What should go in external_reference?
Your own id for the document, something your system can find the record by. On a shared payment link it is shown to the payer, so nothing they should not read.
Do we need webhooks?
No. The event feed carries the same events. Webhooks are faster; the feed is simpler to run. Many integrations use both.
Our server was down for a while.
Read the event feed from your last cursor. If it was down for more than 30 days, list your orders instead.
A name came out spelled wrong.
Reject with the correct spelling in the reason. Next time, send it in name_spellings.
An order sits in awaiting_approval. Will it approve itself?
No. Nothing is approved by a timer. Decide, or the order stays open.
Can we cancel a paid order?
Not through the API. Write to us; if it cannot be completed, it is refunded.
We send for many offices. Do they need Curaword accounts?
No. Send office_reference and office_name on each order. Offices that want to earn from your orders arrange it with us.

19Getting help

Write to your Curaword contact with:

  • the order id, or your external_reference;
  • the time, with its time zone;
  • for an error: the status code, the detail, and the X-Request-Id response header;
  • for a webhook: the Curaword-Delivery id.

Never send us your API key or a signing secret. We will never ask for them.